[
  {
    "path": ".gitignore",
    "content": "node_modules/\ndist/\ndist-ssr/\n\n__pycache__/\n.venv/\nvenv/\n\n.DS_Store\n"
  },
  {
    "path": "README.md",
    "content": "# turbo.art\n\nA playground for creative exploration that uses [SDXL Turbo](https://huggingface.co/stabilityai/sdxl-turbo) for real-time image editing. Try it now at [https://turbo.art](https://turbo.art)!\n\n![turbo-art](https://github.com/modal-labs/turbo-art/assets/5786378/bb185f24-9946-4c26-a7ca-7c8732ea77f0)\n\nThe entire app is serverless and hosted on [Modal](https://modal.com/).\n\n## Developing locally\n\n### File structure\n\n- [turbo_art.py](./turbo_art.py) - model endpoint and FastAPI web server (<150 lines of code!)\n- [src/](./src) - Svelte frontend\n\n### Requirements\n\nTo run this for yourself, you will need:\n\n1. Modal installed and set up locally, as well as FastAPI\n\n```shell\npip install modal fastapi\nmodal setup\n```\n\n2. [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) installed\n\n### Iterate\n\nDuring development, it's useful to have both the frontend and the Modal application automatically react to changes in the code. To do this, you'll need to run two processes.\n\nFirst, in one shell session, run:\n\n```shell\nnpm install\nnpm run build:watch\n```\n\nThen, in another shell session, run:\n\n```shell\nmodal serve turbo_art.py\n```\n\nIn the terminal output, you'll find a URL that you can visit to use your app. While the [`modal serve`](<(https://modal.com/docs/guide/webhooks#developing-with-modal-serve)>) process is running, changes to any of the project files will be automatically applied. `Ctrl+C` will stop the app.\n\n### Deploy\n\nOnce you're happy with your changes, [deploy](https://modal.com/docs/guide/managing-deployments#creating-deployments) your app:\n\n```shell\nnpm run build\nmodal deploy turbo_art.py\n```\n\nIn the terminal output, you'll find a different URL that you can visit to use your app. We chose to use Modal's [custom domains](https://modal.com/docs/guide/webhooks#custom-domains) feature to make the URL more memorable. Without a custom domain, you can still [select](https://modal.com/docs/guide/webhook-urls#user-specified-urls) part of the the `modal.run` subdomain you're assigned.\n\nNote that leaving the app deployed on Modal doesn't cost you anything! Modal apps are serverless and scale to 0 when not in use.\n"
  },
  {
    "path": "index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <link rel=\"icon\" href=\"/assets/favicon.svg\" />\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <meta name=\"description\" content=\"Paint, but with AI!\" />\n\n    <meta name=\"twitter:card\" content=\"summary_large_image\" />\n    <meta name=\"twitter:site\" content=\"@modal_labs\" />\n\n    <meta property=\"og:type\" content=\"website\" />\n    <meta property=\"og:title\" content=\"Turbo.Art\" />\n    <meta property=\"og:description\" content=\"Paint, but with AI!\" />\n    <meta property=\"og:url\" content=\"turbo.art\" />\n    <meta property=\"og:image\" content=\"https://turbo.art/preview.png\" />\n    <link rel=\"stylesheet\" href=\"https://use.typekit.net/jcd8ppx.css\" />\n    <title>Turbo.Art</title>\n    <script>\n      // Should be substituted by the server.\n      window.INFERENCE_BASE_URL = \"{{ inference_url }}\";\n    </script>\n  </head>\n\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"/src/main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"turbo-art\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"vite build\",\n    \"build:watch\": \"vite build --watch\",\n    \"preview\": \"vite preview\",\n    \"check\": \"svelte-check --tsconfig ./tsconfig.json\"\n  },\n  \"devDependencies\": {\n    \"@sveltejs/vite-plugin-svelte\": \"^5.0.3\",\n    \"@tsconfig/svelte\": \"^5.0.2\",\n    \"@types/node\": \"^22.15.19\",\n    \"@types/throttle-debounce\": \"^5.0.2\",\n    \"autoprefixer\": \"^10.4.16\",\n    \"color-picker-svelte\": \"^1.4.0\",\n    \"svelte\": \"^5.30.2\",\n    \"svelte-check\": \"^4.2.1\",\n    \"tailwindcss\": \"^3.4.17\",\n    \"tslib\": \"^2.6.2\",\n    \"typescript\": \"^5.2.2\",\n    \"vite\": \"^6.3.5\"\n  },\n  \"dependencies\": {\n    \"@types/three\": \"^0.176.0\",\n    \"lucide-svelte\": \"^0.511.0\",\n    \"paper\": \"^0.12.17\",\n    \"postprocessing\": \"^6.33.4\",\n    \"three\": \"^0.159.0\",\n    \"throttle-debounce\": \"^5.0.0\"\n  }\n}\n"
  },
  {
    "path": "postcss.config.js",
    "content": "import tailwind from \"tailwindcss\";\nimport tailwindConfig from \"./tailwind.config.js\";\nimport autoprefixer from \"autoprefixer\";\n\nexport default {\n  plugins: [tailwind(tailwindConfig), autoprefixer],\n};\n"
  },
  {
    "path": "src/App.svelte",
    "content": "<script lang=\"ts\">\n  import { Github, Loader, Upload, ArrowUpRight } from \"lucide-svelte\";\n  import { onMount } from \"svelte\";\n  import paper from \"paper\";\n  import { throttle, debounce } from \"throttle-debounce\";\n\n  import modalLogoWithText from \"$lib/assets/logotype.svg\";\n  import Paint from \"$lib/Paint.svelte\";\n  import easterEggImage from \"$lib/assets/mocha_outside.png\";\n  import valleyImg from \"$lib/assets/valley.png\";\n  import turboArtTitleGif from \"$lib/assets/turbo-art-title.gif\";\n  import PreviewImages from \"$lib/PreviewImages.svelte\";\n  import ImageOutput from \"$lib/ImageOutput.svelte\";\n  import Tools from \"$lib/Tools.svelte\";\n\n  const promptOptionsByImage: Record<string, string[]> = {\n    abstract: [\n      \"cityscape, studio ghibli, illustration\",\n      \"a scene from Jodorowsky’s Dune, surreal, sandworm in the background\",\n      \"lunar landing in the style of a van gogh painting\",\n    ],\n    puppy: [\n      \"cartoon bear, pixar, bright, happy\",\n      \"evil cybernetic wolf, watercolor\",\n      \"3d claymation Shiba Inu\",\n    ],\n    car: [\n      \"neon lights, comic book\",\n      \"Tesla driving on the Moon, planets in the background\",\n      \"futuristic car in cyberpunk cityscape, photorealistic\",\n    ],\n    valley: [\n      \"cityscape, studio ghibli, illustration\",\n      \"Mediterranean city, impressionist painting, purple tint\",\n      \"coral reef in the style of Spongebob, cartoon, animated\",\n    ],\n    pasta: [\n      \"coral reef in the style of Spongebob, cartoon, animated\",\n      \"sci-fi scene from star wars, spaceships in the background, cinematic\",\n      \"italian food, cezanne painting\",\n    ],\n  };\n  let value: string = \"\";\n  $: currentImageName = \"valley\";\n  $: promptOptions = promptOptionsByImage[currentImageName];\n\n  let imgInput: HTMLImageElement;\n  let imgOutput: HTMLImageElement;\n  let canvasDrawLayer: HTMLCanvasElement;\n  let inputElement: HTMLInputElement;\n  let fileInput: HTMLInputElement;\n\n  let isImageUploaded = false;\n  let isFirstImageGenerated = false;\n  let isMobile = false;\n  $: numIterations = 0;\n\n  // we track lastUpdatedAt so that expired requests don't overwrite the latest\n  let lastUpdatedAt = 0;\n\n  // used for undo/redo functionality\n  let outputImageHistory: string[] = [];\n  $: currentOutputImageIndex = -1;\n\n  $: isLoading = false;\n  let isInputImageLoading = false;\n\n  let brushSize = \"sm\";\n  let paint = \"#000000\";\n  const radiusByBrushSize: Record<string, number> = {\n    xs: 1,\n    sm: 2,\n    md: 3,\n    lg: 4,\n  };\n\n  function setPaint(e: CustomEvent<string>) {\n    paint = e.detail;\n  }\n  function setBrushSize(e: CustomEvent<string>) {\n    brushSize = e.detail;\n  }\n\n  function checkBreakpoint() {\n    isMobile = window.innerWidth < 640;\n  }\n\n  onMount(() => {\n    // Manual mobile check is needed because of binding issues with <ImageOutput />\n    checkBreakpoint();\n    window.addEventListener(\"resize\", checkBreakpoint);\n\n    /* \n      Setup paper.js for canvas which is a layer above our input image.\n      Paper is used for drawing/paint functionality.\n    */\n    paper.setup(canvasDrawLayer);\n    const tool = new paper.Tool();\n\n    let path: paper.Path;\n\n    tool.onMouseDown = (event: paper.ToolEvent) => {\n      path = new paper.Path();\n      path.strokeColor = new paper.Color(paint);\n      path.strokeWidth = radiusByBrushSize[brushSize] * 4;\n      path.add(event.point);\n\n      throttledgenerateOutputImage();\n    };\n\n    tool.onMouseDrag = (event: paper.ToolEvent) => {\n      path.add(event.point);\n\n      throttledgenerateOutputImage();\n    };\n\n    if (inputElement) {\n      inputElement.focus();\n    }\n\n    setImage(valleyImg);\n    return () => window.removeEventListener(\"resize\", checkBreakpoint);\n  });\n\n  function onLoadInputImg(event: Event) {\n    resizeImage(event);\n\n    isImageUploaded = true;\n    isInputImageLoading = false;\n\n    // kick off an inference on first image load so output image is populated as well\n    // otherwise it will be empty\n    if (!isFirstImageGenerated) {\n      generateOutputImage();\n      isFirstImageGenerated = true;\n    }\n  }\n\n  function setImage(src: string) {\n    isInputImageLoading = true;\n    imgInput.src = src;\n\n    outputImageHistory.unshift(src);\n    currentOutputImageIndex = 0;\n\n    function loopGenerate() {\n      if (isInputImageLoading) {\n        // wait for onload before generating an image\n        setTimeout(loopGenerate, 100);\n        return;\n      }\n\n      generateOutputImage();\n    }\n\n    loopGenerate();\n  }\n\n  function setPrompt(prompt: string) {\n    value = prompt;\n    generateOutputImage();\n  }\n\n  // Our images need to be sized 320x320 for both input and output\n  // This is important because we combine the canvas layer with the image layer\n  // so the pixels need to matchup.\n  function resizeImage(event: Event) {\n    const target = event.target as HTMLImageElement;\n\n    let newWidth;\n    let newHeight;\n    if (target.naturalWidth > target.naturalHeight) {\n      const aspectRatio = target.naturalHeight / target.naturalWidth;\n      newWidth = 320;\n      newHeight = newWidth * aspectRatio;\n    } else {\n      const aspectRatio = target.naturalWidth / target.naturalHeight;\n      newHeight = 320;\n      newWidth = newHeight * aspectRatio;\n    }\n\n    target.style.height = `${newHeight}px`;\n    target.style.width = `${newWidth}px`;\n  }\n\n  function loadImage(e: Event) {\n    const target = e.target as HTMLInputElement;\n    if (!target || !target.files) return;\n    const file = target.files[0];\n\n    if (file) {\n      const reader = new FileReader();\n      reader.onload = (e) => {\n        if (e?.target?.result && typeof e.target.result === \"string\") {\n          isImageUploaded = true;\n          setImage(e?.target.result);\n        }\n      };\n\n      reader.readAsDataURL(file);\n    }\n  }\n\n  function getImageData(useOutputImage: boolean = false): Promise<Blob> {\n    return new Promise((resolve, reject) => {\n      const tempCanvas = document.createElement(\"canvas\");\n      tempCanvas.width = 320;\n      tempCanvas.height = 320;\n      const tempCtx = tempCanvas.getContext(\"2d\");\n      if (!tempCtx) {\n        reject(\"no context\");\n        return;\n      }\n\n      if (useOutputImage) {\n        tempCtx.drawImage(imgOutput, 0, 0, 320, 320);\n      } else {\n        // combines the canvas with the input image so that the\n        // generated image contains edits made by paint brush\n        tempCtx.drawImage(imgInput, 0, 0, 320, 320);\n        tempCtx.drawImage(canvasDrawLayer, 0, 0, 320, 320);\n      }\n\n      tempCanvas.toBlob((blob) => {\n        if (blob) {\n          resolve(blob);\n        } else {\n          reject(\"blob creation failed\");\n        }\n      }, \"image/jpeg\");\n    });\n  }\n\n  const throttledgenerateOutputImage = throttle(\n    250,\n    () => generateOutputImage(),\n    { noLeading: false, noTrailing: false }\n  );\n\n  const debouncedgenerateOutputImage = debounce(\n    100,\n    () => generateOutputImage(),\n    { atBegin: false }\n  );\n\n  function movetoCanvas() {\n    imgInput.src = imgOutput.src;\n  }\n\n  function downloadImage() {\n    let a = document.createElement(\"a\");\n    a.href = imgOutput.src;\n    a.download = \"modal-generated-image.jpeg\";\n    a.click();\n  }\n\n  function enhance() {\n    generateOutputImage(true, 10);\n  }\n\n  function redoOutputImage() {\n    if (currentOutputImageIndex > 0 && outputImageHistory.length > 1) {\n      currentOutputImageIndex -= 1;\n      imgOutput.src = outputImageHistory[currentOutputImageIndex];\n    } else {\n      if (value === \"good boy\") {\n        setImage(easterEggImage);\n        return;\n      }\n      generateOutputImage(true);\n    }\n  }\n\n  function undoOutputImage() {\n    if (currentOutputImageIndex < outputImageHistory.length - 1) {\n      currentOutputImageIndex += 1;\n      imgOutput.src = outputImageHistory[currentOutputImageIndex];\n    }\n  }\n\n  function resetInput() {\n    if (fileInput) {\n      fileInput.value = \"\";\n    }\n  }\n\n  async function generateOutputImage(\n    useOutputImage: boolean = false,\n    iterations: number = 2\n  ) {\n    isLoading = true;\n    const data = await getImageData(useOutputImage);\n\n    const formData = new FormData();\n    formData.append(\"image\", data, \"image.jpg\");\n    formData.append(\"prompt\", value);\n    formData.append(\"num_iterations\", iterations.toString());\n\n    const sentAt = new Date().getTime();\n    try {\n      const res = await fetch((window as any).INFERENCE_BASE_URL, {\n        method: \"POST\",\n        body: formData,\n      });\n      const blob = await res.blob();\n\n      if (sentAt > lastUpdatedAt) {\n        const imageURL = URL.createObjectURL(blob);\n        outputImageHistory = [imageURL, ...outputImageHistory];\n        if (outputImageHistory.length > 10) {\n          outputImageHistory = outputImageHistory.slice(0, 10);\n        }\n        imgOutput.src = imageURL;\n        if (useOutputImage) {\n          numIterations += iterations;\n        } else {\n          numIterations = 1;\n        }\n        lastUpdatedAt = sentAt;\n      }\n\n      isFirstImageGenerated = true;\n    } finally {\n      isLoading = false;\n    }\n  }\n</script>\n\n<main class=\"flex flex-col items-center md:pt-12 text-light-green\">\n  <div class=\"md:max-w-screen-lg lg:w-[1024px] w-full\">\n    <div\n      class=\"bg-light-green/10 sm:border sm:border-light-green/20 md:rounded-lg px-4 py-6 sm:p-6 flex flex-col gap-6\"\n    >\n      <div class=\"flex flex-col gap-3 sm:gap-1\">\n        <div class=\"flex items-center justify-between\">\n          <img width={175} src={turboArtTitleGif} alt=\"Turbo.Art\" />\n          <a\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            href=\"https://github.com/modal-labs/turbo-art/tree/main\"\n            class=\"btns-container justify-center font-medium\"\n          >\n            <Github size={20} />View Code\n          </a>\n        </div>\n        <div class=\"text-sm\">\n          The image generation is powered by Stability's <a\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            class=\"underline\"\n            href=\"https://stability.ai/news/stability-ai-sdxl-turbo\"\n            >SDXL Turbo</a\n          >\n        </div>\n      </div>\n\n      <div class=\"flex flex-col gap-4\">\n        <h3 class=\"heading\">Prompt</h3>\n        <div class=\"flex flex-col sm:flex-row gap-2 lg:flex-nowrap flex-wrap\">\n          {#each promptOptions as item}\n            <button\n              class=\"italic flex-shrink-0 text-xs px-4 py-2 border border-light-green/30 rounded-full text-light-green/60\"\n              class:prompt-active={item === value}\n              on:click={() => setPrompt(item)}>{item}</button\n            >\n          {/each}\n        </div>\n        <input\n          class=\"rounded-full bg-light-green/10 py-4 px-6 w-full text-sm\"\n          bind:value\n          bind:this={inputElement}\n          on:input={debouncedgenerateOutputImage}\n          placeholder=\"Enter prompt here\"\n        />\n      </div>\n\n      <div class=\"flex flex-col md:flex-row gap-6 md:gap-0\">\n        <div\n          class=\"flex flex-col gap-6 md:pr-4 md:border-r md:border-light-green/10 w-full\"\n        >\n          <div class=\"flex flex-col gap-1\">\n            <div class=\"heading flex gap-1 items-center\">\n              Canvas\n              {#if isLoading}\n                <Loader size={14} class=\"animate-spin\" />\n              {/if}\n            </div>\n            <div class=\"text-xs\">Draw on the image to generate a new one</div>\n          </div>\n\n          <div class=\"flex gap-6 flex-col sm:flex-row\">\n            <div>\n              <img\n                alt=\"input\"\n                bind:this={imgInput}\n                class=\"absolute bg-[#D9D9D9] pointer-events-none z-[-1]\"\n                class:hidden={!isImageUploaded}\n                on:load={onLoadInputImg}\n              />\n              <canvas\n                bind:this={canvasDrawLayer}\n                width={320}\n                height={320}\n                class=\"z-1\"\n              ></canvas>\n            </div>\n            {#if isMobile}\n              <ImageOutput\n                bind:imgOutput\n                {isFirstImageGenerated}\n                {resizeImage}\n              />\n            {/if}\n            <div class=\"flex gap-4\">\n              <Paint\n                {paint}\n                {brushSize}\n                on:clearCanvas={() => {\n                  paper.project.activeLayer.removeChildren();\n                  paper.view.update();\n                  generateOutputImage();\n                }}\n                on:setPaint={setPaint}\n                on:setBrushSize={setBrushSize}\n              />\n              <div class=\"sm:hidden block\">\n                <Tools\n                  {undoOutputImage}\n                  {redoOutputImage}\n                  {enhance}\n                  {movetoCanvas}\n                  {downloadImage}\n                />\n              </div>\n            </div>\n          </div>\n          <div class=\"flex gap-2\">\n            <PreviewImages\n              {promptOptionsByImage}\n              {imgInput}\n              {setImage}\n              setCurrentImage={(name) => (currentImageName = name)}\n              setPrompt={(v) => (value = v)}\n            />\n          </div>\n          <input\n            type=\"file\"\n            accept=\"image/*\"\n            id=\"file-upload\"\n            hidden\n            bind:this={fileInput}\n            on:change={loadImage}\n            on:click={resetInput}\n          />\n          <label\n            for=\"file-upload\"\n            class=\"btns-container flex-col w-fit cursor-pointer w-full sm:w-fit\"\n          >\n            <div class=\"flex items-center gap-2 font-medium\">\n              <Upload size={16} />\n              Upload Image (JPG, PNG)\n            </div>\n          </label>\n        </div>\n\n        <div class=\"flex flex-col gap-6 w-full md:pl-6\">\n          <div class=\"flex flex-col gap-1 sm:block hidden\">\n            <div class=\"flex items-center gap-1 heading\">\n              Output\n              {#if isLoading}\n                <Loader size={14} class=\"animate-spin\" />\n              {/if}\n            </div>\n            <div class=\"text-xs\">\n              Generated Image (iterations: {numIterations})\n            </div>\n          </div>\n\n          <div class=\"flex gap-4 flex-col sm:flex-row md:flex-col lg:flex-row\">\n            {#if !isMobile}\n              <ImageOutput\n                bind:imgOutput\n                {isFirstImageGenerated}\n                {resizeImage}\n              />\n            {/if}\n            <div class=\"sm:block hidden\">\n              <Tools\n                {undoOutputImage}\n                {redoOutputImage}\n                {enhance}\n                {movetoCanvas}\n                {downloadImage}\n              />\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <div\n      class=\"lg:w-full flex mt-6 mb-8 md:mb-12 mx-2 md:mx-6 lg:mx-0 justify-between items-center\"\n    >\n      <div class=\"flex items-center gap-3 font-degular\">\n        Built with <img\n          class=\"modal-logo\"\n          alt=\"Modal logo\"\n          src={modalLogoWithText}\n        />\n      </div>\n      <a\n        target=\"_blank\"\n        rel=\"noopener noreferrer\"\n        href=\"https://modal.com\"\n        class=\"button px-5 py-[6px] font-medium\"\n      >\n        Get Started <ArrowUpRight size={16} />\n      </a>\n    </div>\n  </div>\n</main>\n\n<style lang=\"postcss\">\n  .heading {\n    @apply text-2xl font-degular;\n  }\n\n  .btns-container {\n    @apply flex items-center gap-2 py-2 px-6 border rounded-full border-light-green/30 text-sm;\n  }\n\n  .button {\n    @apply bg-primary rounded-full justify-center items-center flex gap-2 text-black text-sm;\n  }\n\n  .modal-logo {\n    width: 108px;\n    height: 32px;\n  }\n\n  .prompt-active {\n    @apply text-light-green border-light-green/80;\n  }\n</style>\n"
  },
  {
    "path": "src/app.css",
    "content": "/* #RefactorExamplesComponentsAndStyles */\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  body {\n    @apply bg-black;\n    color-scheme: dark;\n    text-rendering: optimizelegibility;\n    -moz-osx-font-smoothing: grayscale;\n    -webkit-font-smoothing: antialiased;\n  }\n\n  * {\n    @apply outline-transparent;\n  }\n\n  *:focus {\n    outline: none;\n  }\n}\n"
  },
  {
    "path": "src/lib/ImageOutput.svelte",
    "content": "<script lang=\"ts\">\n  export let imgOutput: HTMLImageElement;\n  export let isFirstImageGenerated: boolean;\n  export let resizeImage: (event: Event) => void;\n</script>\n\n<img\n  width={320}\n  height={320}\n  alt=\"loading...\"\n  bind:this={imgOutput}\n  class=\"bg-[#D9D9D9]\"\n  class:hidden={!isFirstImageGenerated}\n  on:load={resizeImage}\n/>\n"
  },
  {
    "path": "src/lib/Paint.svelte",
    "content": "<script lang=\"ts\">\n  import { Eraser } from \"lucide-svelte\";\n  import { Color } from \"color-picker-svelte\";\n\n  import smallPaintIcon from \"$lib/assets/sm-paint-icon.svg\";\n  import extraSmallPaintIcon from \"$lib/assets/xs-paint-icon.svg\";\n  import mediumPaintIcon from \"$lib/assets/md-paint-icon.svg\";\n  import largePaintIcon from \"$lib/assets/lg-paint-icon.svg\";\n  import { createEventDispatcher } from \"svelte\";\n\n  const dispatch = createEventDispatcher<{\n    clearCanvas: void;\n    setPaint: string;\n    setBrushSize: string;\n  }>();\n\n  export let paint: string;\n  export let brushSize: string;\n  $: color = new Color(paint);\n  $: {\n    handleSetPaint(color.toHexString());\n  }\n\n  const handleClearCanvas = () => {\n    dispatch(\"clearCanvas\");\n  };\n\n  const handleSetPaint = (paint: string) => {\n    dispatch(\"setPaint\", paint);\n  };\n\n  const handleSetBrushSize = (size: string) => {\n    dispatch(\"setBrushSize\", size);\n  };\n\n  const palette = [\n    { hex: \"#000000\", name: \"black\" },\n    { hex: \"#ffffff\", name: \"white\" },\n    { hex: \"#2613fd\", name: \"blue\" },\n    { hex: \"#5e57b3\", name: \"indigo\" },\n    { hex: \"#fd139f\", name: \"pink\" },\n    { hex: \"#a4fd13\", name: \"lime\" },\n    { hex: \"#fd1321\", name: \"red\" },\n    { hex: \"#ff9900\", name: \"orange\" },\n  ];\n</script>\n\n<div class=\"flex flex-col gap-3\">\n  <button\n    class=\"tool gap-2.5 w-full text-xs\"\n    on:click={() => handleClearCanvas()}\n  >\n    <Eraser size={16} /> Clear\n  </button>\n\n  <div class=\"tool flex-col\">\n    <div class=\"grid grid-cols-2 gap-2\">\n      {#each palette as { hex, name }}\n        <button\n          aria-label=\"{name} paint\"\n          on:click={() => handleSetPaint(hex)}\n          class=\"circle\"\n          class:circle-active={paint === hex}\n          style:background={hex}\n        ></button>\n      {/each}\n    </div>\n\n    <hr class=\"border-light-green/10\" />\n\n    <div class=\"grid grid-cols-2 gap-1 w-full\">\n      <button class=\"brush-btn\" on:click={() => handleSetBrushSize(\"xs\")}>\n        <img\n          class=\"brush\"\n          class:brush-active={brushSize === \"xs\"}\n          src={extraSmallPaintIcon}\n          alt=\"extrasmall paint icon\"\n        />\n      </button>\n      <button class=\"brush-btn\" on:click={() => handleSetBrushSize(\"sm\")}>\n        <img\n          class=\"brush\"\n          class:brush-active={brushSize === \"sm\"}\n          src={smallPaintIcon}\n          alt=\"small paint icon\"\n        />\n      </button>\n      <button class=\"brush-btn\" on:click={() => handleSetBrushSize(\"md\")}>\n        <img\n          class=\"brush\"\n          class:brush-active={brushSize === \"md\"}\n          src={mediumPaintIcon}\n          alt=\"medium paint icon\"\n        />\n      </button>\n      <button class=\"brush-btn\" on:click={() => handleSetBrushSize(\"lg\")}>\n        <img\n          class=\"brush\"\n          class:brush-active={brushSize === \"lg\"}\n          src={largePaintIcon}\n          alt=\"large paint icon\"\n        />\n      </button>\n    </div>\n  </div>\n</div>\n\n<style lang=\"postcss\">\n  .circle {\n    @apply w-5 h-5 rounded-full;\n  }\n\n  .circle-active {\n    @apply border-[1px] border-light-green;\n  }\n\n  .brush-btn {\n    @apply w-6 h-5 flex items-center justify-center;\n  }\n\n  .brush {\n    filter: invert(100%) sepia(6%) saturate(7487%) hue-rotate(293deg)\n      brightness(103%) contrast(118%);\n  }\n\n  .brush-active {\n    filter: brightness(0) saturate(100%) invert(64%) sepia(27%) saturate(194%)\n      hue-rotate(69deg) brightness(89%) contrast(89%);\n  }\n\n  .tool {\n    @apply flex gap-2.5 py-2 px-3 border rounded-[10px] border-light-green/5 bg-light-green/10 w-fit;\n  }\n</style>\n"
  },
  {
    "path": "src/lib/PreviewImages.svelte",
    "content": "<script lang=\"ts\">\n  import carImage from \"$lib/assets/car.png\";\n  import valleyImage from \"$lib/assets/valley.png\";\n  import puppyImage from \"$lib/assets/puppy.png\";\n  import pastaImage from \"$lib/assets/pasta.png\";\n  import abstractImage from \"$lib/assets/abstract.png\";\n\n  export let promptOptionsByImage: Record<string, string[]>;\n  export let imgInput: HTMLImageElement;\n\n  export let setImage: (src: string) => void;\n  export let setPrompt: (value: string) => void;\n  export let setCurrentImage: (name: string) => void;\n</script>\n\n<button\n  on:click={() => setCurrentImage(\"valley\")}\n  on:click={() => setImage(valleyImage)}\n  on:click={() => setPrompt(promptOptionsByImage[\"valley\"][0])}\n>\n  <img\n    class=\"w-14 h-14 bg-gray\"\n    class:preview-active={imgInput?.src.includes(valleyImage)}\n    src={valleyImage}\n    alt=\"preview img\"\n  />\n</button>\n<button\n  on:click={() => setCurrentImage(\"puppy\")}\n  on:click={() => setImage(puppyImage)}\n  on:click={() => setPrompt(promptOptionsByImage[\"puppy\"][0])}\n>\n  <img\n    class=\"w-14 h-14 bg-gray\"\n    class:preview-active={imgInput?.src.includes(puppyImage)}\n    src={puppyImage}\n    alt=\"preview img puppy\"\n  />\n</button>\n<button\n  on:click={() => setCurrentImage(\"car\")}\n  on:click={() => setImage(carImage)}\n  on:click={() => setPrompt(promptOptionsByImage[\"car\"][0])}\n>\n  <img\n    class=\"w-14 h-14 bg-gray\"\n    class:preview-active={imgInput?.src.includes(carImage)}\n    src={carImage}\n    alt=\"preview img car\"\n  />\n</button>\n<button\n  on:click={() => setCurrentImage(\"abstract\")}\n  on:click={() => setImage(abstractImage)}\n  on:click={() => setPrompt(promptOptionsByImage[\"abstract\"][0])}\n>\n  <img\n    class=\"w-14 h-14 bg-gray\"\n    class:preview-active={imgInput?.src.includes(abstractImage)}\n    src={abstractImage}\n    alt=\"preview img\"\n  />\n</button>\n<button\n  on:click={() => setCurrentImage(\"pasta\")}\n  on:click={() => setImage(pastaImage)}\n  on:click={() => setPrompt(promptOptionsByImage[\"pasta\"][0])}\n>\n  <img\n    class=\"w-14 h-14 bg-gray\"\n    class:preview-active={imgInput?.src.includes(pastaImage)}\n    src={pastaImage}\n    alt=\"preview img\"\n  />\n</button>\n\n<style lang=\"postcss\">\n  .preview-active {\n    @apply border-[1px] border-light-green;\n  }\n</style>\n"
  },
  {
    "path": "src/lib/Tools.svelte",
    "content": "<script lang=\"ts\">\n  import {\n    Undo,\n    Redo,\n    ArrowDownToLine,\n    ArrowLeftSquare,\n    Wand,\n  } from \"lucide-svelte\";\n\n  export let undoOutputImage: () => void;\n  export let redoOutputImage: () => void;\n  export let enhance: () => void;\n  export let movetoCanvas: () => void;\n  export let downloadImage: () => void;\n</script>\n\n<div class=\"flex flex-col sm:gap-3 gap-2\">\n  <div class=\"tool\">\n    <button class=\"text-xs flex gap-1\" on:click={undoOutputImage}>\n      <Undo size={16} /> Back\n    </button>\n    <div class=\"w-[1px] h-4 bg-white/10\"></div>\n    <button class=\"text-xs flex gap-1\" on:click={redoOutputImage}>\n      <Redo size={16} /> Next\n    </button>\n  </div>\n  <button\n    class=\"special-button text-xs tool yellow-background\"\n    on:click={enhance}\n  >\n    <Wand size={16} /> Enhance\n  </button>\n  <button class=\"text-xs tool\" on:click={movetoCanvas}>\n    <ArrowLeftSquare size={16} /> Move to Canvas\n  </button>\n\n  <button class=\"text-xs tool\" on:click={downloadImage}>\n    <ArrowDownToLine size={16} /> Download\n  </button>\n</div>\n\n<style lang=\"postcss\">\n  .tool {\n    @apply flex gap-1.5 py-2 px-2 border rounded-[10px] border-light-green/5 bg-light-green/10 w-fit;\n  }\n\n  @media (min-width: 1024px) {\n    .tool {\n      @apply w-full whitespace-nowrap;\n    }\n  }\n\n  .yellow-background {\n    @apply bg-muted-yellow text-gray-900;\n  }\n</style>\n"
  },
  {
    "path": "src/main.ts",
    "content": "import { mount } from \"svelte\";\nimport App from \"./App.svelte\";\nimport \"./app.css\";\n\nconst app = mount(App, {\n  target: document.getElementById(\"app\")!,\n});\n\nexport default app;\n"
  },
  {
    "path": "src/vite-env.d.ts",
    "content": "/// <reference types=\"svelte\" />\n/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "svelte.config.js",
    "content": "import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'\n\nexport default {\n  // Consult https://svelte.dev/docs#compile-time-svelte-preprocess\n  // for more information about preprocessors\n  preprocess: vitePreprocess(),\n}\n"
  },
  {
    "path": "tailwind.config.js",
    "content": "import defaultTheme from \"tailwindcss/defaultTheme\";\n\n/** @type {import('tailwindcss').Config} */\nexport default {\n  content: [\"./index.html\", \"./src/**/*.{html,js,svelte,ts}\"],\n  theme: {\n    extend: {\n      screens: {\n        md: \"840px\",\n        xl: \"1200px\",\n      },\n      colors: {\n        // Theme colors\n        primary: \"#7FEE64\",\n        \"light-green\": \"#DDFFDC\",\n        \"muted-yellow\": \"#FFEA71\",\n      },\n      fontFamily: {\n        mono: [\"Fira Mono\", ...defaultTheme.fontFamily.mono],\n        sans: [\"Inter Variable\", ...defaultTheme.fontFamily.sans],\n        inter: [\"Inter Variable\", ...defaultTheme.fontFamily.sans],\n        tosh: [\"Tosh Modal\", ...defaultTheme.fontFamily.sans],\n        degular: [\"degular\", ...defaultTheme.fontFamily.sans],\n      },\n\n      // Global font modifications\n      fontSize: {\n        sm: [\n          \"0.875rem\",\n          {\n            lineHeight: \"1.25rem\",\n            letterSpacing: \"0.01em\",\n          },\n        ],\n      },\n    },\n  },\n  plugins: [],\n};\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"extends\": \"@tsconfig/svelte/tsconfig.json\",\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"useDefineForClassFields\": true,\n    \"module\": \"ESNext\",\n    \"resolveJsonModule\": true,\n    /**\n     * Typecheck JS in `.svelte` and `.js` files by default.\n     * Disable checkJs if you'd like to use dynamic types in JS.\n     * Note that setting allowJs false does not prevent the use\n     * of JS in `.svelte` files.\n     */\n    \"allowJs\": true,\n    \"checkJs\": true,\n    \"isolatedModules\": true,\n    \"paths\": {\n      \"$lib\": [\"./src/lib\"], // aliased by SvelteKit\n      \"$lib/*\": [\"./src/lib/*\"],\n    }\n  },\n  \"include\": [\"src/**/*.ts\", \"src/**/*.js\", \"src/**/*.svelte\"],\n  \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\"\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "turbo_art.py",
    "content": "from pathlib import Path\n\nimport modal\nfrom fastapi import FastAPI, File, Form, Response, UploadFile\nfrom fastapi.staticfiles import StaticFiles\n\napp = modal.App(\"stable-diffusion-xl-turbo\")\n\nassets_path = Path(__file__).parent / \"dist\"\n\nweb_image = (\n    modal.Image.debian_slim()\n    .pip_install(\"jinja2\", \"fastapi[standard]\")\n    .add_local_dir(assets_path, \"/assets\")\n)\n\n\ndef download_weights():\n    from huggingface_hub import snapshot_download\n\n    # Ignore files that we don't need to speed up download time.\n    ignore = [\n        \"*.bin\",\n        \"*.onnx_data\",\n        \"*/diffusion_pytorch_model.safetensors\",\n    ]\n\n    snapshot_download(\"stabilityai/sdxl-turbo\", ignore_patterns=ignore)\n\n    # https://huggingface.co/docs/diffusers/main/en/using-diffusers/sdxl_turbo#speed-up-sdxl-turbo-even-more\n    # vae is used for a inference speedup\n    snapshot_download(\"madebyollin/sdxl-vae-fp16-fix\", ignore_patterns=ignore)\n\n\ninference_image = (\n    modal.Image.debian_slim()\n    .pip_install(\n        \"Pillow~=10.1.0\",\n        \"diffusers~=0.24\",\n        \"transformers~=4.35\",\n        \"accelerate~=0.25\",\n        \"safetensors~=0.4\",\n        \"fastapi[standard]\",\n    )\n    .run_function(download_weights)\n)\n\nwith inference_image.imports():\n    from io import BytesIO\n\n    import torch\n    from diffusers import AutoencoderKL, AutoPipelineForImage2Image\n    from diffusers.utils import load_image\n    from PIL import Image\n\n\n@app.cls(\n    gpu=\"A100\",\n    image=inference_image,\n    scaledown_window=240,\n    max_containers=10,\n)\nclass Model:\n    @modal.enter()\n    def enter(self):\n        self.pipe = AutoPipelineForImage2Image.from_pretrained(\n            \"stabilityai/sdxl-turbo\",\n            torch_dtype=torch.float16,\n            device_map=\"balanced\",\n            variant=\"fp16\",\n            vae=AutoencoderKL.from_pretrained(\n                \"madebyollin/sdxl-vae-fp16-fix\",\n                torch_dtype=torch.float16,\n                device_map=\"balanced\",\n            ),\n        )\n\n        # We execute a blank inference since there are objects that are lazily loaded that\n        # we want to start loading before an actual user query\n        self.pipe(\n            \"blank\",\n            image=Image.new(\"RGB\", (800, 1280), (255, 255, 255)),\n            num_inference_steps=1,\n            strength=1,\n            guidance_scale=0.0,\n            seed=42,\n        )\n\n    @modal.fastapi_endpoint(method=\"POST\")\n    async def inference(\n        self,\n        image: UploadFile = File(...),\n        prompt: str = Form(...),\n        num_iterations: str = Form(...),\n    ):\n        img_data_in = await image.read()\n\n        init_image = load_image(Image.open(BytesIO(img_data_in))).resize((512, 512))\n        # based on trial and error we saw the best results with 3 inference steps\n        # it had better generation results than 4,5,6 even though it's faster\n        num_inference_steps = int(num_iterations)\n        # note: anything under 0.5 strength gives blurry results\n        strength = 0.999 if num_iterations == 2 else 0.65\n        assert num_inference_steps * strength >= 1\n\n        image = self.pipe(\n            prompt,\n            image=init_image,\n            num_inference_steps=num_inference_steps,\n            strength=strength,\n            guidance_scale=0.0,\n            seed=42,\n        ).images[0]\n\n        byte_stream = BytesIO()\n        image.save(byte_stream, format=\"jpeg\")\n        img_data_out = byte_stream.getvalue()\n\n        return Response(content=img_data_out, media_type=\"image/jpeg\")\n\n\n@app.function(image=web_image)\n@modal.concurrent(max_inputs=10)\n@modal.asgi_app()\ndef fastapi_app():\n    import jinja2\n\n    web_app = FastAPI()\n\n    with open(\"/assets/index.html\", \"r\") as f:\n        template_html = f.read()\n\n    template = jinja2.Template(template_html)\n\n    with open(\"/assets/index.html\", \"w\") as f:\n        # Mount the {{ inference_url }} with the Modal inference endpoint.\n        html = template.render(inference_url=Model.inference.web_url)\n        f.write(html)\n\n    web_app.mount(\"/\", StaticFiles(directory=\"/assets\", html=True))\n\n    return web_app\n"
  },
  {
    "path": "vite.config.ts",
    "content": "import postcss from \"./postcss.config.js\";\nimport { defineConfig } from \"vite\";\nimport { svelte } from \"@sveltejs/vite-plugin-svelte\";\nimport path from \"node:path\";\n\nexport default defineConfig({\n  plugins: [svelte()],\n  css: {\n    postcss,\n  },\n  build: {\n    minify: true,\n  },\n  server: {\n    open: true,\n  },\n  resolve: {\n    alias: {\n      $lib: path.resolve(\"./src/lib\"),\n    },\n  },\n});\n"
  }
]