Full Code of modal-labs/turbo-art for AI

main c75ee541ed6f cached
19 files
34.0 KB
9.5k tokens
5 symbols
1 requests
Download .txt
Repository: modal-labs/turbo-art
Branch: main
Commit: c75ee541ed6f
Files: 19
Total size: 34.0 KB

Directory structure:
gitextract_sj_tsbtw/

├── .gitignore
├── README.md
├── index.html
├── package.json
├── postcss.config.js
├── src/
│   ├── App.svelte
│   ├── app.css
│   ├── lib/
│   │   ├── ImageOutput.svelte
│   │   ├── Paint.svelte
│   │   ├── PreviewImages.svelte
│   │   └── Tools.svelte
│   ├── main.ts
│   └── vite-env.d.ts
├── svelte.config.js
├── tailwind.config.js
├── tsconfig.json
├── tsconfig.node.json
├── turbo_art.py
└── vite.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
node_modules/
dist/
dist-ssr/

__pycache__/
.venv/
venv/

.DS_Store


================================================
FILE: README.md
================================================
# turbo.art

A 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)!

![turbo-art](https://github.com/modal-labs/turbo-art/assets/5786378/bb185f24-9946-4c26-a7ca-7c8732ea77f0)

The entire app is serverless and hosted on [Modal](https://modal.com/).

## Developing locally

### File structure

- [turbo_art.py](./turbo_art.py) - model endpoint and FastAPI web server (<150 lines of code!)
- [src/](./src) - Svelte frontend

### Requirements

To run this for yourself, you will need:

1. Modal installed and set up locally, as well as FastAPI

```shell
pip install modal fastapi
modal setup
```

2. [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) installed

### Iterate

During 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.

First, in one shell session, run:

```shell
npm install
npm run build:watch
```

Then, in another shell session, run:

```shell
modal serve turbo_art.py
```

In 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.

### Deploy

Once you're happy with your changes, [deploy](https://modal.com/docs/guide/managing-deployments#creating-deployments) your app:

```shell
npm run build
modal deploy turbo_art.py
```

In 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.

Note that leaving the app deployed on Modal doesn't cost you anything! Modal apps are serverless and scale to 0 when not in use.


================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <link rel="icon" href="/assets/favicon.svg" />
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="Paint, but with AI!" />

    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:site" content="@modal_labs" />

    <meta property="og:type" content="website" />
    <meta property="og:title" content="Turbo.Art" />
    <meta property="og:description" content="Paint, but with AI!" />
    <meta property="og:url" content="turbo.art" />
    <meta property="og:image" content="https://turbo.art/preview.png" />
    <link rel="stylesheet" href="https://use.typekit.net/jcd8ppx.css" />
    <title>Turbo.Art</title>
    <script>
      // Should be substituted by the server.
      window.INFERENCE_BASE_URL = "{{ inference_url }}";
    </script>
  </head>

  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>


================================================
FILE: package.json
================================================
{
  "name": "turbo-art",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "build:watch": "vite build --watch",
    "preview": "vite preview",
    "check": "svelte-check --tsconfig ./tsconfig.json"
  },
  "devDependencies": {
    "@sveltejs/vite-plugin-svelte": "^5.0.3",
    "@tsconfig/svelte": "^5.0.2",
    "@types/node": "^22.15.19",
    "@types/throttle-debounce": "^5.0.2",
    "autoprefixer": "^10.4.16",
    "color-picker-svelte": "^1.4.0",
    "svelte": "^5.30.2",
    "svelte-check": "^4.2.1",
    "tailwindcss": "^3.4.17",
    "tslib": "^2.6.2",
    "typescript": "^5.2.2",
    "vite": "^6.3.5"
  },
  "dependencies": {
    "@types/three": "^0.176.0",
    "lucide-svelte": "^0.511.0",
    "paper": "^0.12.17",
    "postprocessing": "^6.33.4",
    "three": "^0.159.0",
    "throttle-debounce": "^5.0.0"
  }
}


================================================
FILE: postcss.config.js
================================================
import tailwind from "tailwindcss";
import tailwindConfig from "./tailwind.config.js";
import autoprefixer from "autoprefixer";

export default {
  plugins: [tailwind(tailwindConfig), autoprefixer],
};


================================================
FILE: src/App.svelte
================================================
<script lang="ts">
  import { Github, Loader, Upload, ArrowUpRight } from "lucide-svelte";
  import { onMount } from "svelte";
  import paper from "paper";
  import { throttle, debounce } from "throttle-debounce";

  import modalLogoWithText from "$lib/assets/logotype.svg";
  import Paint from "$lib/Paint.svelte";
  import easterEggImage from "$lib/assets/mocha_outside.png";
  import valleyImg from "$lib/assets/valley.png";
  import turboArtTitleGif from "$lib/assets/turbo-art-title.gif";
  import PreviewImages from "$lib/PreviewImages.svelte";
  import ImageOutput from "$lib/ImageOutput.svelte";
  import Tools from "$lib/Tools.svelte";

  const promptOptionsByImage: Record<string, string[]> = {
    abstract: [
      "cityscape, studio ghibli, illustration",
      "a scene from Jodorowsky’s Dune, surreal, sandworm in the background",
      "lunar landing in the style of a van gogh painting",
    ],
    puppy: [
      "cartoon bear, pixar, bright, happy",
      "evil cybernetic wolf, watercolor",
      "3d claymation Shiba Inu",
    ],
    car: [
      "neon lights, comic book",
      "Tesla driving on the Moon, planets in the background",
      "futuristic car in cyberpunk cityscape, photorealistic",
    ],
    valley: [
      "cityscape, studio ghibli, illustration",
      "Mediterranean city, impressionist painting, purple tint",
      "coral reef in the style of Spongebob, cartoon, animated",
    ],
    pasta: [
      "coral reef in the style of Spongebob, cartoon, animated",
      "sci-fi scene from star wars, spaceships in the background, cinematic",
      "italian food, cezanne painting",
    ],
  };
  let value: string = "";
  $: currentImageName = "valley";
  $: promptOptions = promptOptionsByImage[currentImageName];

  let imgInput: HTMLImageElement;
  let imgOutput: HTMLImageElement;
  let canvasDrawLayer: HTMLCanvasElement;
  let inputElement: HTMLInputElement;
  let fileInput: HTMLInputElement;

  let isImageUploaded = false;
  let isFirstImageGenerated = false;
  let isMobile = false;
  $: numIterations = 0;

  // we track lastUpdatedAt so that expired requests don't overwrite the latest
  let lastUpdatedAt = 0;

  // used for undo/redo functionality
  let outputImageHistory: string[] = [];
  $: currentOutputImageIndex = -1;

  $: isLoading = false;
  let isInputImageLoading = false;

  let brushSize = "sm";
  let paint = "#000000";
  const radiusByBrushSize: Record<string, number> = {
    xs: 1,
    sm: 2,
    md: 3,
    lg: 4,
  };

  function setPaint(e: CustomEvent<string>) {
    paint = e.detail;
  }
  function setBrushSize(e: CustomEvent<string>) {
    brushSize = e.detail;
  }

  function checkBreakpoint() {
    isMobile = window.innerWidth < 640;
  }

  onMount(() => {
    // Manual mobile check is needed because of binding issues with <ImageOutput />
    checkBreakpoint();
    window.addEventListener("resize", checkBreakpoint);

    /* 
      Setup paper.js for canvas which is a layer above our input image.
      Paper is used for drawing/paint functionality.
    */
    paper.setup(canvasDrawLayer);
    const tool = new paper.Tool();

    let path: paper.Path;

    tool.onMouseDown = (event: paper.ToolEvent) => {
      path = new paper.Path();
      path.strokeColor = new paper.Color(paint);
      path.strokeWidth = radiusByBrushSize[brushSize] * 4;
      path.add(event.point);

      throttledgenerateOutputImage();
    };

    tool.onMouseDrag = (event: paper.ToolEvent) => {
      path.add(event.point);

      throttledgenerateOutputImage();
    };

    if (inputElement) {
      inputElement.focus();
    }

    setImage(valleyImg);
    return () => window.removeEventListener("resize", checkBreakpoint);
  });

  function onLoadInputImg(event: Event) {
    resizeImage(event);

    isImageUploaded = true;
    isInputImageLoading = false;

    // kick off an inference on first image load so output image is populated as well
    // otherwise it will be empty
    if (!isFirstImageGenerated) {
      generateOutputImage();
      isFirstImageGenerated = true;
    }
  }

  function setImage(src: string) {
    isInputImageLoading = true;
    imgInput.src = src;

    outputImageHistory.unshift(src);
    currentOutputImageIndex = 0;

    function loopGenerate() {
      if (isInputImageLoading) {
        // wait for onload before generating an image
        setTimeout(loopGenerate, 100);
        return;
      }

      generateOutputImage();
    }

    loopGenerate();
  }

  function setPrompt(prompt: string) {
    value = prompt;
    generateOutputImage();
  }

  // Our images need to be sized 320x320 for both input and output
  // This is important because we combine the canvas layer with the image layer
  // so the pixels need to matchup.
  function resizeImage(event: Event) {
    const target = event.target as HTMLImageElement;

    let newWidth;
    let newHeight;
    if (target.naturalWidth > target.naturalHeight) {
      const aspectRatio = target.naturalHeight / target.naturalWidth;
      newWidth = 320;
      newHeight = newWidth * aspectRatio;
    } else {
      const aspectRatio = target.naturalWidth / target.naturalHeight;
      newHeight = 320;
      newWidth = newHeight * aspectRatio;
    }

    target.style.height = `${newHeight}px`;
    target.style.width = `${newWidth}px`;
  }

  function loadImage(e: Event) {
    const target = e.target as HTMLInputElement;
    if (!target || !target.files) return;
    const file = target.files[0];

    if (file) {
      const reader = new FileReader();
      reader.onload = (e) => {
        if (e?.target?.result && typeof e.target.result === "string") {
          isImageUploaded = true;
          setImage(e?.target.result);
        }
      };

      reader.readAsDataURL(file);
    }
  }

  function getImageData(useOutputImage: boolean = false): Promise<Blob> {
    return new Promise((resolve, reject) => {
      const tempCanvas = document.createElement("canvas");
      tempCanvas.width = 320;
      tempCanvas.height = 320;
      const tempCtx = tempCanvas.getContext("2d");
      if (!tempCtx) {
        reject("no context");
        return;
      }

      if (useOutputImage) {
        tempCtx.drawImage(imgOutput, 0, 0, 320, 320);
      } else {
        // combines the canvas with the input image so that the
        // generated image contains edits made by paint brush
        tempCtx.drawImage(imgInput, 0, 0, 320, 320);
        tempCtx.drawImage(canvasDrawLayer, 0, 0, 320, 320);
      }

      tempCanvas.toBlob((blob) => {
        if (blob) {
          resolve(blob);
        } else {
          reject("blob creation failed");
        }
      }, "image/jpeg");
    });
  }

  const throttledgenerateOutputImage = throttle(
    250,
    () => generateOutputImage(),
    { noLeading: false, noTrailing: false }
  );

  const debouncedgenerateOutputImage = debounce(
    100,
    () => generateOutputImage(),
    { atBegin: false }
  );

  function movetoCanvas() {
    imgInput.src = imgOutput.src;
  }

  function downloadImage() {
    let a = document.createElement("a");
    a.href = imgOutput.src;
    a.download = "modal-generated-image.jpeg";
    a.click();
  }

  function enhance() {
    generateOutputImage(true, 10);
  }

  function redoOutputImage() {
    if (currentOutputImageIndex > 0 && outputImageHistory.length > 1) {
      currentOutputImageIndex -= 1;
      imgOutput.src = outputImageHistory[currentOutputImageIndex];
    } else {
      if (value === "good boy") {
        setImage(easterEggImage);
        return;
      }
      generateOutputImage(true);
    }
  }

  function undoOutputImage() {
    if (currentOutputImageIndex < outputImageHistory.length - 1) {
      currentOutputImageIndex += 1;
      imgOutput.src = outputImageHistory[currentOutputImageIndex];
    }
  }

  function resetInput() {
    if (fileInput) {
      fileInput.value = "";
    }
  }

  async function generateOutputImage(
    useOutputImage: boolean = false,
    iterations: number = 2
  ) {
    isLoading = true;
    const data = await getImageData(useOutputImage);

    const formData = new FormData();
    formData.append("image", data, "image.jpg");
    formData.append("prompt", value);
    formData.append("num_iterations", iterations.toString());

    const sentAt = new Date().getTime();
    try {
      const res = await fetch((window as any).INFERENCE_BASE_URL, {
        method: "POST",
        body: formData,
      });
      const blob = await res.blob();

      if (sentAt > lastUpdatedAt) {
        const imageURL = URL.createObjectURL(blob);
        outputImageHistory = [imageURL, ...outputImageHistory];
        if (outputImageHistory.length > 10) {
          outputImageHistory = outputImageHistory.slice(0, 10);
        }
        imgOutput.src = imageURL;
        if (useOutputImage) {
          numIterations += iterations;
        } else {
          numIterations = 1;
        }
        lastUpdatedAt = sentAt;
      }

      isFirstImageGenerated = true;
    } finally {
      isLoading = false;
    }
  }
</script>

<main class="flex flex-col items-center md:pt-12 text-light-green">
  <div class="md:max-w-screen-lg lg:w-[1024px] w-full">
    <div
      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"
    >
      <div class="flex flex-col gap-3 sm:gap-1">
        <div class="flex items-center justify-between">
          <img width={175} src={turboArtTitleGif} alt="Turbo.Art" />
          <a
            target="_blank"
            rel="noopener noreferrer"
            href="https://github.com/modal-labs/turbo-art/tree/main"
            class="btns-container justify-center font-medium"
          >
            <Github size={20} />View Code
          </a>
        </div>
        <div class="text-sm">
          The image generation is powered by Stability's <a
            target="_blank"
            rel="noopener noreferrer"
            class="underline"
            href="https://stability.ai/news/stability-ai-sdxl-turbo"
            >SDXL Turbo</a
          >
        </div>
      </div>

      <div class="flex flex-col gap-4">
        <h3 class="heading">Prompt</h3>
        <div class="flex flex-col sm:flex-row gap-2 lg:flex-nowrap flex-wrap">
          {#each promptOptions as item}
            <button
              class="italic flex-shrink-0 text-xs px-4 py-2 border border-light-green/30 rounded-full text-light-green/60"
              class:prompt-active={item === value}
              on:click={() => setPrompt(item)}>{item}</button
            >
          {/each}
        </div>
        <input
          class="rounded-full bg-light-green/10 py-4 px-6 w-full text-sm"
          bind:value
          bind:this={inputElement}
          on:input={debouncedgenerateOutputImage}
          placeholder="Enter prompt here"
        />
      </div>

      <div class="flex flex-col md:flex-row gap-6 md:gap-0">
        <div
          class="flex flex-col gap-6 md:pr-4 md:border-r md:border-light-green/10 w-full"
        >
          <div class="flex flex-col gap-1">
            <div class="heading flex gap-1 items-center">
              Canvas
              {#if isLoading}
                <Loader size={14} class="animate-spin" />
              {/if}
            </div>
            <div class="text-xs">Draw on the image to generate a new one</div>
          </div>

          <div class="flex gap-6 flex-col sm:flex-row">
            <div>
              <img
                alt="input"
                bind:this={imgInput}
                class="absolute bg-[#D9D9D9] pointer-events-none z-[-1]"
                class:hidden={!isImageUploaded}
                on:load={onLoadInputImg}
              />
              <canvas
                bind:this={canvasDrawLayer}
                width={320}
                height={320}
                class="z-1"
              ></canvas>
            </div>
            {#if isMobile}
              <ImageOutput
                bind:imgOutput
                {isFirstImageGenerated}
                {resizeImage}
              />
            {/if}
            <div class="flex gap-4">
              <Paint
                {paint}
                {brushSize}
                on:clearCanvas={() => {
                  paper.project.activeLayer.removeChildren();
                  paper.view.update();
                  generateOutputImage();
                }}
                on:setPaint={setPaint}
                on:setBrushSize={setBrushSize}
              />
              <div class="sm:hidden block">
                <Tools
                  {undoOutputImage}
                  {redoOutputImage}
                  {enhance}
                  {movetoCanvas}
                  {downloadImage}
                />
              </div>
            </div>
          </div>
          <div class="flex gap-2">
            <PreviewImages
              {promptOptionsByImage}
              {imgInput}
              {setImage}
              setCurrentImage={(name) => (currentImageName = name)}
              setPrompt={(v) => (value = v)}
            />
          </div>
          <input
            type="file"
            accept="image/*"
            id="file-upload"
            hidden
            bind:this={fileInput}
            on:change={loadImage}
            on:click={resetInput}
          />
          <label
            for="file-upload"
            class="btns-container flex-col w-fit cursor-pointer w-full sm:w-fit"
          >
            <div class="flex items-center gap-2 font-medium">
              <Upload size={16} />
              Upload Image (JPG, PNG)
            </div>
          </label>
        </div>

        <div class="flex flex-col gap-6 w-full md:pl-6">
          <div class="flex flex-col gap-1 sm:block hidden">
            <div class="flex items-center gap-1 heading">
              Output
              {#if isLoading}
                <Loader size={14} class="animate-spin" />
              {/if}
            </div>
            <div class="text-xs">
              Generated Image (iterations: {numIterations})
            </div>
          </div>

          <div class="flex gap-4 flex-col sm:flex-row md:flex-col lg:flex-row">
            {#if !isMobile}
              <ImageOutput
                bind:imgOutput
                {isFirstImageGenerated}
                {resizeImage}
              />
            {/if}
            <div class="sm:block hidden">
              <Tools
                {undoOutputImage}
                {redoOutputImage}
                {enhance}
                {movetoCanvas}
                {downloadImage}
              />
            </div>
          </div>
        </div>
      </div>
    </div>

    <div
      class="lg:w-full flex mt-6 mb-8 md:mb-12 mx-2 md:mx-6 lg:mx-0 justify-between items-center"
    >
      <div class="flex items-center gap-3 font-degular">
        Built with <img
          class="modal-logo"
          alt="Modal logo"
          src={modalLogoWithText}
        />
      </div>
      <a
        target="_blank"
        rel="noopener noreferrer"
        href="https://modal.com"
        class="button px-5 py-[6px] font-medium"
      >
        Get Started <ArrowUpRight size={16} />
      </a>
    </div>
  </div>
</main>

<style lang="postcss">
  .heading {
    @apply text-2xl font-degular;
  }

  .btns-container {
    @apply flex items-center gap-2 py-2 px-6 border rounded-full border-light-green/30 text-sm;
  }

  .button {
    @apply bg-primary rounded-full justify-center items-center flex gap-2 text-black text-sm;
  }

  .modal-logo {
    width: 108px;
    height: 32px;
  }

  .prompt-active {
    @apply text-light-green border-light-green/80;
  }
</style>


================================================
FILE: src/app.css
================================================
/* #RefactorExamplesComponentsAndStyles */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  body {
    @apply bg-black;
    color-scheme: dark;
    text-rendering: optimizelegibility;
    -moz-osx-font-smoothing: grayscale;
    -webkit-font-smoothing: antialiased;
  }

  * {
    @apply outline-transparent;
  }

  *:focus {
    outline: none;
  }
}


================================================
FILE: src/lib/ImageOutput.svelte
================================================
<script lang="ts">
  export let imgOutput: HTMLImageElement;
  export let isFirstImageGenerated: boolean;
  export let resizeImage: (event: Event) => void;
</script>

<img
  width={320}
  height={320}
  alt="loading..."
  bind:this={imgOutput}
  class="bg-[#D9D9D9]"
  class:hidden={!isFirstImageGenerated}
  on:load={resizeImage}
/>


================================================
FILE: src/lib/Paint.svelte
================================================
<script lang="ts">
  import { Eraser } from "lucide-svelte";
  import { Color } from "color-picker-svelte";

  import smallPaintIcon from "$lib/assets/sm-paint-icon.svg";
  import extraSmallPaintIcon from "$lib/assets/xs-paint-icon.svg";
  import mediumPaintIcon from "$lib/assets/md-paint-icon.svg";
  import largePaintIcon from "$lib/assets/lg-paint-icon.svg";
  import { createEventDispatcher } from "svelte";

  const dispatch = createEventDispatcher<{
    clearCanvas: void;
    setPaint: string;
    setBrushSize: string;
  }>();

  export let paint: string;
  export let brushSize: string;
  $: color = new Color(paint);
  $: {
    handleSetPaint(color.toHexString());
  }

  const handleClearCanvas = () => {
    dispatch("clearCanvas");
  };

  const handleSetPaint = (paint: string) => {
    dispatch("setPaint", paint);
  };

  const handleSetBrushSize = (size: string) => {
    dispatch("setBrushSize", size);
  };

  const palette = [
    { hex: "#000000", name: "black" },
    { hex: "#ffffff", name: "white" },
    { hex: "#2613fd", name: "blue" },
    { hex: "#5e57b3", name: "indigo" },
    { hex: "#fd139f", name: "pink" },
    { hex: "#a4fd13", name: "lime" },
    { hex: "#fd1321", name: "red" },
    { hex: "#ff9900", name: "orange" },
  ];
</script>

<div class="flex flex-col gap-3">
  <button
    class="tool gap-2.5 w-full text-xs"
    on:click={() => handleClearCanvas()}
  >
    <Eraser size={16} /> Clear
  </button>

  <div class="tool flex-col">
    <div class="grid grid-cols-2 gap-2">
      {#each palette as { hex, name }}
        <button
          aria-label="{name} paint"
          on:click={() => handleSetPaint(hex)}
          class="circle"
          class:circle-active={paint === hex}
          style:background={hex}
        ></button>
      {/each}
    </div>

    <hr class="border-light-green/10" />

    <div class="grid grid-cols-2 gap-1 w-full">
      <button class="brush-btn" on:click={() => handleSetBrushSize("xs")}>
        <img
          class="brush"
          class:brush-active={brushSize === "xs"}
          src={extraSmallPaintIcon}
          alt="extrasmall paint icon"
        />
      </button>
      <button class="brush-btn" on:click={() => handleSetBrushSize("sm")}>
        <img
          class="brush"
          class:brush-active={brushSize === "sm"}
          src={smallPaintIcon}
          alt="small paint icon"
        />
      </button>
      <button class="brush-btn" on:click={() => handleSetBrushSize("md")}>
        <img
          class="brush"
          class:brush-active={brushSize === "md"}
          src={mediumPaintIcon}
          alt="medium paint icon"
        />
      </button>
      <button class="brush-btn" on:click={() => handleSetBrushSize("lg")}>
        <img
          class="brush"
          class:brush-active={brushSize === "lg"}
          src={largePaintIcon}
          alt="large paint icon"
        />
      </button>
    </div>
  </div>
</div>

<style lang="postcss">
  .circle {
    @apply w-5 h-5 rounded-full;
  }

  .circle-active {
    @apply border-[1px] border-light-green;
  }

  .brush-btn {
    @apply w-6 h-5 flex items-center justify-center;
  }

  .brush {
    filter: invert(100%) sepia(6%) saturate(7487%) hue-rotate(293deg)
      brightness(103%) contrast(118%);
  }

  .brush-active {
    filter: brightness(0) saturate(100%) invert(64%) sepia(27%) saturate(194%)
      hue-rotate(69deg) brightness(89%) contrast(89%);
  }

  .tool {
    @apply flex gap-2.5 py-2 px-3 border rounded-[10px] border-light-green/5 bg-light-green/10 w-fit;
  }
</style>


================================================
FILE: src/lib/PreviewImages.svelte
================================================
<script lang="ts">
  import carImage from "$lib/assets/car.png";
  import valleyImage from "$lib/assets/valley.png";
  import puppyImage from "$lib/assets/puppy.png";
  import pastaImage from "$lib/assets/pasta.png";
  import abstractImage from "$lib/assets/abstract.png";

  export let promptOptionsByImage: Record<string, string[]>;
  export let imgInput: HTMLImageElement;

  export let setImage: (src: string) => void;
  export let setPrompt: (value: string) => void;
  export let setCurrentImage: (name: string) => void;
</script>

<button
  on:click={() => setCurrentImage("valley")}
  on:click={() => setImage(valleyImage)}
  on:click={() => setPrompt(promptOptionsByImage["valley"][0])}
>
  <img
    class="w-14 h-14 bg-gray"
    class:preview-active={imgInput?.src.includes(valleyImage)}
    src={valleyImage}
    alt="preview img"
  />
</button>
<button
  on:click={() => setCurrentImage("puppy")}
  on:click={() => setImage(puppyImage)}
  on:click={() => setPrompt(promptOptionsByImage["puppy"][0])}
>
  <img
    class="w-14 h-14 bg-gray"
    class:preview-active={imgInput?.src.includes(puppyImage)}
    src={puppyImage}
    alt="preview img puppy"
  />
</button>
<button
  on:click={() => setCurrentImage("car")}
  on:click={() => setImage(carImage)}
  on:click={() => setPrompt(promptOptionsByImage["car"][0])}
>
  <img
    class="w-14 h-14 bg-gray"
    class:preview-active={imgInput?.src.includes(carImage)}
    src={carImage}
    alt="preview img car"
  />
</button>
<button
  on:click={() => setCurrentImage("abstract")}
  on:click={() => setImage(abstractImage)}
  on:click={() => setPrompt(promptOptionsByImage["abstract"][0])}
>
  <img
    class="w-14 h-14 bg-gray"
    class:preview-active={imgInput?.src.includes(abstractImage)}
    src={abstractImage}
    alt="preview img"
  />
</button>
<button
  on:click={() => setCurrentImage("pasta")}
  on:click={() => setImage(pastaImage)}
  on:click={() => setPrompt(promptOptionsByImage["pasta"][0])}
>
  <img
    class="w-14 h-14 bg-gray"
    class:preview-active={imgInput?.src.includes(pastaImage)}
    src={pastaImage}
    alt="preview img"
  />
</button>

<style lang="postcss">
  .preview-active {
    @apply border-[1px] border-light-green;
  }
</style>


================================================
FILE: src/lib/Tools.svelte
================================================
<script lang="ts">
  import {
    Undo,
    Redo,
    ArrowDownToLine,
    ArrowLeftSquare,
    Wand,
  } from "lucide-svelte";

  export let undoOutputImage: () => void;
  export let redoOutputImage: () => void;
  export let enhance: () => void;
  export let movetoCanvas: () => void;
  export let downloadImage: () => void;
</script>

<div class="flex flex-col sm:gap-3 gap-2">
  <div class="tool">
    <button class="text-xs flex gap-1" on:click={undoOutputImage}>
      <Undo size={16} /> Back
    </button>
    <div class="w-[1px] h-4 bg-white/10"></div>
    <button class="text-xs flex gap-1" on:click={redoOutputImage}>
      <Redo size={16} /> Next
    </button>
  </div>
  <button
    class="special-button text-xs tool yellow-background"
    on:click={enhance}
  >
    <Wand size={16} /> Enhance
  </button>
  <button class="text-xs tool" on:click={movetoCanvas}>
    <ArrowLeftSquare size={16} /> Move to Canvas
  </button>

  <button class="text-xs tool" on:click={downloadImage}>
    <ArrowDownToLine size={16} /> Download
  </button>
</div>

<style lang="postcss">
  .tool {
    @apply flex gap-1.5 py-2 px-2 border rounded-[10px] border-light-green/5 bg-light-green/10 w-fit;
  }

  @media (min-width: 1024px) {
    .tool {
      @apply w-full whitespace-nowrap;
    }
  }

  .yellow-background {
    @apply bg-muted-yellow text-gray-900;
  }
</style>


================================================
FILE: src/main.ts
================================================
import { mount } from "svelte";
import App from "./App.svelte";
import "./app.css";

const app = mount(App, {
  target: document.getElementById("app")!,
});

export default app;


================================================
FILE: src/vite-env.d.ts
================================================
/// <reference types="svelte" />
/// <reference types="vite/client" />


================================================
FILE: svelte.config.js
================================================
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'

export default {
  // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
  // for more information about preprocessors
  preprocess: vitePreprocess(),
}


================================================
FILE: tailwind.config.js
================================================
import defaultTheme from "tailwindcss/defaultTheme";

/** @type {import('tailwindcss').Config} */
export default {
  content: ["./index.html", "./src/**/*.{html,js,svelte,ts}"],
  theme: {
    extend: {
      screens: {
        md: "840px",
        xl: "1200px",
      },
      colors: {
        // Theme colors
        primary: "#7FEE64",
        "light-green": "#DDFFDC",
        "muted-yellow": "#FFEA71",
      },
      fontFamily: {
        mono: ["Fira Mono", ...defaultTheme.fontFamily.mono],
        sans: ["Inter Variable", ...defaultTheme.fontFamily.sans],
        inter: ["Inter Variable", ...defaultTheme.fontFamily.sans],
        tosh: ["Tosh Modal", ...defaultTheme.fontFamily.sans],
        degular: ["degular", ...defaultTheme.fontFamily.sans],
      },

      // Global font modifications
      fontSize: {
        sm: [
          "0.875rem",
          {
            lineHeight: "1.25rem",
            letterSpacing: "0.01em",
          },
        ],
      },
    },
  },
  plugins: [],
};


================================================
FILE: tsconfig.json
================================================
{
  "extends": "@tsconfig/svelte/tsconfig.json",
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "resolveJsonModule": true,
    /**
     * Typecheck JS in `.svelte` and `.js` files by default.
     * Disable checkJs if you'd like to use dynamic types in JS.
     * Note that setting allowJs false does not prevent the use
     * of JS in `.svelte` files.
     */
    "allowJs": true,
    "checkJs": true,
    "isolatedModules": true,
    "paths": {
      "$lib": ["./src/lib"], // aliased by SvelteKit
      "$lib/*": ["./src/lib/*"],
    }
  },
  "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
  "references": [{ "path": "./tsconfig.node.json" }]
}


================================================
FILE: tsconfig.node.json
================================================
{
  "compilerOptions": {
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "bundler"
  },
  "include": ["vite.config.ts"]
}


================================================
FILE: turbo_art.py
================================================
from pathlib import Path

import modal
from fastapi import FastAPI, File, Form, Response, UploadFile
from fastapi.staticfiles import StaticFiles

app = modal.App("stable-diffusion-xl-turbo")

assets_path = Path(__file__).parent / "dist"

web_image = (
    modal.Image.debian_slim()
    .pip_install("jinja2", "fastapi[standard]")
    .add_local_dir(assets_path, "/assets")
)


def download_weights():
    from huggingface_hub import snapshot_download

    # Ignore files that we don't need to speed up download time.
    ignore = [
        "*.bin",
        "*.onnx_data",
        "*/diffusion_pytorch_model.safetensors",
    ]

    snapshot_download("stabilityai/sdxl-turbo", ignore_patterns=ignore)

    # https://huggingface.co/docs/diffusers/main/en/using-diffusers/sdxl_turbo#speed-up-sdxl-turbo-even-more
    # vae is used for a inference speedup
    snapshot_download("madebyollin/sdxl-vae-fp16-fix", ignore_patterns=ignore)


inference_image = (
    modal.Image.debian_slim()
    .pip_install(
        "Pillow~=10.1.0",
        "diffusers~=0.24",
        "transformers~=4.35",
        "accelerate~=0.25",
        "safetensors~=0.4",
        "fastapi[standard]",
    )
    .run_function(download_weights)
)

with inference_image.imports():
    from io import BytesIO

    import torch
    from diffusers import AutoencoderKL, AutoPipelineForImage2Image
    from diffusers.utils import load_image
    from PIL import Image


@app.cls(
    gpu="A100",
    image=inference_image,
    scaledown_window=240,
    max_containers=10,
)
class Model:
    @modal.enter()
    def enter(self):
        self.pipe = AutoPipelineForImage2Image.from_pretrained(
            "stabilityai/sdxl-turbo",
            torch_dtype=torch.float16,
            device_map="balanced",
            variant="fp16",
            vae=AutoencoderKL.from_pretrained(
                "madebyollin/sdxl-vae-fp16-fix",
                torch_dtype=torch.float16,
                device_map="balanced",
            ),
        )

        # We execute a blank inference since there are objects that are lazily loaded that
        # we want to start loading before an actual user query
        self.pipe(
            "blank",
            image=Image.new("RGB", (800, 1280), (255, 255, 255)),
            num_inference_steps=1,
            strength=1,
            guidance_scale=0.0,
            seed=42,
        )

    @modal.fastapi_endpoint(method="POST")
    async def inference(
        self,
        image: UploadFile = File(...),
        prompt: str = Form(...),
        num_iterations: str = Form(...),
    ):
        img_data_in = await image.read()

        init_image = load_image(Image.open(BytesIO(img_data_in))).resize((512, 512))
        # based on trial and error we saw the best results with 3 inference steps
        # it had better generation results than 4,5,6 even though it's faster
        num_inference_steps = int(num_iterations)
        # note: anything under 0.5 strength gives blurry results
        strength = 0.999 if num_iterations == 2 else 0.65
        assert num_inference_steps * strength >= 1

        image = self.pipe(
            prompt,
            image=init_image,
            num_inference_steps=num_inference_steps,
            strength=strength,
            guidance_scale=0.0,
            seed=42,
        ).images[0]

        byte_stream = BytesIO()
        image.save(byte_stream, format="jpeg")
        img_data_out = byte_stream.getvalue()

        return Response(content=img_data_out, media_type="image/jpeg")


@app.function(image=web_image)
@modal.concurrent(max_inputs=10)
@modal.asgi_app()
def fastapi_app():
    import jinja2

    web_app = FastAPI()

    with open("/assets/index.html", "r") as f:
        template_html = f.read()

    template = jinja2.Template(template_html)

    with open("/assets/index.html", "w") as f:
        # Mount the {{ inference_url }} with the Modal inference endpoint.
        html = template.render(inference_url=Model.inference.web_url)
        f.write(html)

    web_app.mount("/", StaticFiles(directory="/assets", html=True))

    return web_app


================================================
FILE: vite.config.ts
================================================
import postcss from "./postcss.config.js";
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import path from "node:path";

export default defineConfig({
  plugins: [svelte()],
  css: {
    postcss,
  },
  build: {
    minify: true,
  },
  server: {
    open: true,
  },
  resolve: {
    alias: {
      $lib: path.resolve("./src/lib"),
    },
  },
});
Download .txt
gitextract_sj_tsbtw/

├── .gitignore
├── README.md
├── index.html
├── package.json
├── postcss.config.js
├── src/
│   ├── App.svelte
│   ├── app.css
│   ├── lib/
│   │   ├── ImageOutput.svelte
│   │   ├── Paint.svelte
│   │   ├── PreviewImages.svelte
│   │   └── Tools.svelte
│   ├── main.ts
│   └── vite-env.d.ts
├── svelte.config.js
├── tailwind.config.js
├── tsconfig.json
├── tsconfig.node.json
├── turbo_art.py
└── vite.config.ts
Download .txt
SYMBOL INDEX (5 symbols across 1 files)

FILE: turbo_art.py
  function download_weights (line 18) | def download_weights():
  class Model (line 63) | class Model:
    method enter (line 65) | def enter(self):
    method inference (line 90) | async def inference(
  function fastapi_app (line 125) | def fastapi_app():
Condensed preview — 19 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (38K chars).
[
  {
    "path": ".gitignore",
    "chars": 68,
    "preview": "node_modules/\ndist/\ndist-ssr/\n\n__pycache__/\n.venv/\nvenv/\n\n.DS_Store\n"
  },
  {
    "path": "README.md",
    "chars": 2169,
    "preview": "# turbo.art\n\nA playground for creative exploration that uses [SDXL Turbo](https://huggingface.co/stabilityai/sdxl-turbo)"
  },
  {
    "path": "index.html",
    "chars": 1027,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <link rel=\"icon\" href=\"/assets/favicon.svg\" />\n    <meta charset=\"UTF-8\" /"
  },
  {
    "path": "package.json",
    "chars": 895,
    "preview": "{\n  \"name\": \"turbo-art\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n"
  },
  {
    "path": "postcss.config.js",
    "chars": 202,
    "preview": "import tailwind from \"tailwindcss\";\nimport tailwindConfig from \"./tailwind.config.js\";\nimport autoprefixer from \"autopre"
  },
  {
    "path": "src/App.svelte",
    "chars": 15693,
    "preview": "<script lang=\"ts\">\n  import { Github, Loader, Upload, ArrowUpRight } from \"lucide-svelte\";\n  import { onMount } from \"sv"
  },
  {
    "path": "src/app.css",
    "chars": 377,
    "preview": "/* #RefactorExamplesComponentsAndStyles */\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  b"
  },
  {
    "path": "src/lib/ImageOutput.svelte",
    "chars": 334,
    "preview": "<script lang=\"ts\">\n  export let imgOutput: HTMLImageElement;\n  export let isFirstImageGenerated: boolean;\n  export let r"
  },
  {
    "path": "src/lib/Paint.svelte",
    "chars": 3567,
    "preview": "<script lang=\"ts\">\n  import { Eraser } from \"lucide-svelte\";\n  import { Color } from \"color-picker-svelte\";\n\n  import sm"
  },
  {
    "path": "src/lib/PreviewImages.svelte",
    "chars": 2228,
    "preview": "<script lang=\"ts\">\n  import carImage from \"$lib/assets/car.png\";\n  import valleyImage from \"$lib/assets/valley.png\";\n  i"
  },
  {
    "path": "src/lib/Tools.svelte",
    "chars": 1367,
    "preview": "<script lang=\"ts\">\n  import {\n    Undo,\n    Redo,\n    ArrowDownToLine,\n    ArrowLeftSquare,\n    Wand,\n  } from \"lucide-s"
  },
  {
    "path": "src/main.ts",
    "chars": 178,
    "preview": "import { mount } from \"svelte\";\nimport App from \"./App.svelte\";\nimport \"./app.css\";\n\nconst app = mount(App, {\n  target: "
  },
  {
    "path": "src/vite-env.d.ts",
    "chars": 71,
    "preview": "/// <reference types=\"svelte\" />\n/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "svelte.config.js",
    "chars": 228,
    "preview": "import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'\n\nexport default {\n  // Consult https://svelte.dev/docs#com"
  },
  {
    "path": "tailwind.config.js",
    "chars": 1007,
    "preview": "import defaultTheme from \"tailwindcss/defaultTheme\";\n\n/** @type {import('tailwindcss').Config} */\nexport default {\n  con"
  },
  {
    "path": "tsconfig.json",
    "chars": 729,
    "preview": "{\n  \"extends\": \"@tsconfig/svelte/tsconfig.json\",\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"useDefineForClassFi"
  },
  {
    "path": "tsconfig.node.json",
    "chars": 171,
    "preview": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\""
  },
  {
    "path": "turbo_art.py",
    "chars": 4102,
    "preview": "from pathlib import Path\n\nimport modal\nfrom fastapi import FastAPI, File, Form, Response, UploadFile\nfrom fastapi.static"
  },
  {
    "path": "vite.config.ts",
    "chars": 394,
    "preview": "import postcss from \"./postcss.config.js\";\nimport { defineConfig } from \"vite\";\nimport { svelte } from \"@sveltejs/vite-p"
  }
]

About this extraction

This page contains the full source code of the modal-labs/turbo-art GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 19 files (34.0 KB), approximately 9.5k tokens, and a symbol index with 5 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!