# Building an AI Image Generator with OpenAI

> Source: <https://dev.to/anvil/building-an-ai-image-generator-with-openai-5541>
> Published: 2026-09-09 15:59:56+00:00

In this tutorial, we're going to build a web application that uses [OpenAI's Images API](https://platform.openai.com/docs/api-reference/images) to turn drawings into photorealistic images. We'll build the entire app using Python with Anvil.

When the app is finished, we'll be able to upload an image and click a button to call the Images API. When the model has finished generating a new image, it will be displayed on the screen, and we can download it.

To build the app, we will:

For this tutorial, you will need basic Python knowledge and an OpenAI account with API credits.

If you'd prefer to follow the video version of this tutorial, you can find that here: [https://www.youtube.com/watch?v=imJ_YaSHxOk](https://www.youtube.com/watch?v=imJ_YaSHxOk)

Let's get started!

[Log in](https://anvil.works/login?utm_source=crosspost:dev.to:/learn/tutorials/open-ai) to Anvil and click 'Create a new app'. Choose the New M3 theme and select 'Blank Panel Form'.

If you can't find the New M3 theme, you may need to open the "Advanced" dropdown

First, rename the Form to "MainForm" by right-clicking on it in the App Browser, then rename the app. Click on the name at the top of the screen and give it a name like "OpenAI Image Generator".

We're now looking at the [Form Editor](https://anvil.works/docs/editor/form-editor?utm_source=crosspost:dev.to:/learn/tutorials/open-ai), where we can drag and drop components from the [Toolbox](https://anvil.works/docs/editor/form-editor#toolbox?utm_source=crosspost:dev.to:/learn/tutorials/open-ai) to build our app's UI.

Let's start by adding a Card to the Form to hold our images and buttons. Drop a [ColumnPanel](https://anvil.works/docs/ui/components/containers#columnpanel?utm_source=crosspost:dev.to:/learn/tutorials/open-ai) inside the Card to make it easier to lay out the components.

We need a button to upload an image - that's what the [FileLoader component](https://anvil.works/docs/ui/app-themes/material-3/components#fileloader) is for. Drag and drop a FileLoader into the ColumnPanel.

Centre-align the FileLoader using the floating [Object Palette](https://anvil.works/docs/editor/form-editor#object-palette). From the [Properties Panel](https://anvil.works/docs/editor/form-editor#properties-panel), change its appearance property to `filled`. We only want to upload images, so set the `file_types` property to `image/*`.

We need an [Image component](https://anvil.works/docs/ui/components/basic#image) to display the uploaded image. Drag and drop an Image component above the FileLoader. Change its name to `uploaded_img`.

We don't want it visible until an image is uploaded, so click the eye icon on the Object Palette to make it invisible. Also set its `display_mode` property to `fill_width`.

Add another Image component next to the first one to display our generated image. Name it `output_img` and make it invisible too.

We now need a button that, when clicked, will call the OpenAI API. Add a [Button component](https://anvil.works/docs/ui/app-themes/material-3/components#button) to the page and name it `generate_button`. Centre it, change the text to "Turn into photo", and make it invisible to start. We'll make the Button visible once a file is uploaded.

Let's write some code that will make the `uploaded_img` and `generate_button` components visible when an image is uploaded.

Select the FileLoader and click `on change event` from the Object Palette. This opens the code view and automatically creates a method that runs when a file is uploaded.

Add the following code:

```
@handle("file_loader_1", "change")
def self.file_loader_1_change(self, file, **event_args):
    """This method is called when a new file is loaded into this FileLoader"""
    if file:
        self.uploaded_img.source = file
        self.uploaded_img.visible = True
        self.generate_button.visible = True
```

Let's test out our UI and the code we just wrote. At the top right of the Anvil Editor, click the green Run button. Upload an image and you should see it appear along with the "Turn into photo" button.

We're going to use the [Images API](https://platform.openai.com/docs/api-reference/images) from OpenAI to turn our drawings into photorealistic images. In order to use this API, we need to get an API key from OpenAI.

If you don't already have an OpenAI account, create one at [platform.openai.com](https://platform.openai.com).

Once logged in, go to the Billing page and add credits to your account. The minimum amount that you can add is typically $5, which is plenty for building and testing this app.

You'll need to verify your account to use the Images API. In the "General" tab, look for a "Verify Organization" button.

If you don't see the verification button immediately, you may need to wait a few days for it to appear. Once verified, it will say "Organization verified".

Navigate to "API keys" and click "Create new secret key". Give it a name and copy the key that appears. You won't be able to see the key again after navigating away.

We can now store the key securely in our Anvil app. Back in the app, click the blue '+' button in the Sidebar Menu and choose "App Secrets".

Click "Create new secret" and name it `OPEN_AI_API_KEY`. Click "Set value" and paste in your API key from OpenAI. This key is now encrypted and stored securely.

We can now set up our backend to call the API and get a generated image.

`openai` pacakage
First, we need to install the OpenAI Python package in our app's server environment.

In the [Sidebar Menu](https://anvil.works/docs/editor#sidebar-menu), navigate to Settings and select "Python versions". Switch the base package to "Machine Learning", and in the packages section, add `openai`. You can leave the version box blank.

Back in the App Browser, click "Add Server Module" to add a server environment to your app. This is a Python environment running on Anvil's secure cloud servers.

Let's first copy and paste the code from the Images API documentation that can be found [here](https://platform.openai.com/docs/guides/image-generation?api=image#edit-images). Add your API key that's stored in App Secrets to the OpenAI client:

```
client = OpenAI(api_key=anvil.secrets.get_secret('OPEN_AI_API_KEY'))
```

We'll need to modify this code so that it works in Anvil but for now, your server code should look like this:

``` python
import anvil.secrets
import anvil.server
import base64
from openai import OpenAI
client = OpenAI(api_key=anvil.secrets.get_secret('OPEN_AI_API_KEY'))

prompt = """
Generate a photorealistic image of a gift basket on a white background 
labeled 'Relax & Unwind' with a ribbon and handwriting-like font, 
containing all the items in the reference pictures.
"""

result = client.images.edit(
    model="gpt-image-1",
    image=[
        open("body-lotion.png", "rb"),
        open("bath-bomb.png", "rb"),
        open("incense-kit.png", "rb"),
        open("soap.png", "rb"),
    ],
    prompt=prompt
)

image_base64 = result.data[0].b64_json
image_bytes = base64.b64decode(image_base64)
```

An Anvil server module doesn’t run top to bottom like a normal Python script. Any code we write here will run when it’s called, so we need to turn this code into a function. We’ll then call that function when the `generate_button` is clicked.

Create a function called `generate_image` that takes in `input_img` as an argument. Indent the code we copied into this function and change the prompt to say "Turn the drawing into a photorealistic image". Your server code should now look something like this:

``` python
import anvil.secrets
import anvil.server
import base64
from openai import OpenAI
client = OpenAI(api_key=anvil.secrets.get_secret('OPEN_AI_API_KEY'))

def generate_image(input_img):
    result = client.images.edit(
        model="gpt-image-1",
        image=[
            open("body-lotion.png", "rb"),
            open("bath-bomb.png", "rb"),
            open("incense-kit.png", "rb"),
            open("soap.png", "rb"),
        ],
        prompt="Turn the drawing into a photorealistic image"
    )

    image_base64 = result.data[0].b64_json
    image_bytes = base64.b64decode(image_base64)

    # Save the image to a file
    with open("gift-basket.png", "wb") as f:
        f.write(image_bytes)
```

We need to pass in a file path for our input image into the OpenAI API. We can get a temporary file path using [`anvil.media.TempFile`](https://anvil.works/docs/working-with-files/media/files_on_disk#media-object-to-temporary-file). 

OpenAI checks the MIME type of images based on the file extension, not the actual content, so we also need to append the proper file extension to our temporary file path.

Add the following import statements to your server code:

``` python
import anvil.media
import mimetypes
import os
```

Then, inside `generate_image`, we'll get the MIME type of `input_img` and use `mimetypes` to find the corresponding file extension. We then need to create a `TempFile` and rename the filepath so that it includes this extension:

``` python
def generate_image(input_img):
    #get the MIME type and extension from the input_img
    mime_type = input_img.content_type
    ext = mimetypes.guess_extension(mime_type)
    #create a temporary file path
    with anvil.media.TempFile(input_img) as tmp_path:
        #add the extension to the temporary path
        new_path = tmp_path + ext
        result = client.images.edit(
            model="gpt-image-1",
            image=[
                #pass the path to the model
                open(new_path, "rb"),
            ],
            prompt="Turn the drawing into a photorealistic image"
        )

        image_base64 = result.data[0].b64_json
        image_bytes = base64.b64decode(image_base64)

        # Save the image to a file
        with open("gift-basket.png", "wb") as f:
            f.write(image_bytes)
```

When the model finishes generating an image, we can create an Anvil [Media Object](https://anvil.works/docs/working-with-files/media) instead of writing to a file. Replace the `with` statement at the end of the server function with:

```
output_img =  anvil.BlobMedia(content_type="text/jpeg", content=image_bytes, name="ai-image.jpg")
```

The `name` argument will be the name of your file when downloaded. You can change this name to anything you'd like.

Your server code should now look like this:

``` python
import anvil.secrets
import anvil.server
import base64
from openai import OpenAI
import anvil.media
import mimetypes
import os

client = OpenAI(api_key=anvil.secrets.get_secret('OPEN_AI_API_KEY'))

def generate_image(input_img):
    #get the MIME type and extension from the input_img
    mime_type = input_img.content_type
    ext = mimetypes.guess_extension(mime_type)
    #create a temporary file path
    with anvil.media.TempFile(input_img) as tmp_path:
        #add the extension to the temporary path
        new_path = tmp_path + ext
        result = client.images.edit(
            model="gpt-image-1",
            image=[
                #pass the path to the model
                open(new_path, "rb"),
            ],
            prompt="Turn the drawing into a photorealistic image"
        )

    image_base64 = result.data[0].b64_json
    image_bytes = base64.b64decode(image_base64)

    # Save the image to a file
    output_img =  anvil.BlobMedia(content_type="text/jpeg", content=image_bytes, name="ai-image.jpg")
```

The image generation request may take some time, so we don't want our server to hang while waiting. To prevent this, we can run the function in the background using [Background Tasks](https://anvil.works/docs/background-tasks?utm_source=crosspost:dev.to:/learn/tutorials/open-ai).

To turn the function into a background task, we just need to decorate it with `@anvil.server.background_task`.

``` python
@anvil.server.background_task
def generate_image(input_img):
    ...
```

We want to be able to launch the background task when the `generate_button` is clicked. To do that, we need to create a client-callable function that launches the background task. 

In the ServerModule, add the following function

``` python
@anvil.server.callable
def launch_bg_task(input_img):
    task = anvil.server.launch_background_task('generate_image', input_img)
    return task
```

The `@anvil.server.callable` decorator makes this function callable from our frontend code. 

`anvil.server.launch_background_task` returns a [Task object](https://anvil.works/docs/background-tasks/communicating-back#task-object), which we can use to check when the background task is finished and get the return value.

[We can't return a Media object directly from a background task](https://anvil.works/docs/server/background-tasks/communicating-back#communicating-back-to-the-main-program), so we'll store the generated image in a Data Table.

Choose Data from the Sidebar Menu and click "Add Table" to create a new [Data Table](https://anvil.works/docs/data-tables?utm_source=crosspost:dev.to:/learn/tutorials/open-ai). Call this table `tasks` and add the following columns:

`image` (Media column) - for the generated image`task_id` (Text column) - for the background task ID
When the image has finished generating, we need to add a row to the Data Table and return the row. Add the following lines of code to the bottom of the `generate_image` function:

```
task_id = anvil.server.context.background_task_id
row = app_tables.tasks.add_row(image=output_img, task_id=task_id)
return row
```

Your finished ServerModule should now look something like this:

``` python
import anvil.secrets
import anvil.server
import base64
from openai import OpenAI
import anvil.media
import mimetypes
import os

client = OpenAI(api_key=anvil.secrets.get_secret('OPEN_AI_API_KEY'))

@anvil.server.callable
def launch_bg_task(input_img):
    task = anvil.server.launch_background_task('generate_image', input_img)
    return task

@anvil.server.background_task
def generate_image(input_img):
    #get the MIME type and extension from the input_img
    mime_type = input_img.content_type
    ext = mimetypes.guess_extension(mime_type)
    #create a temporary file path
    with anvil.media.TempFile(input_img) as tmp_path:
        #add the extension to the temporary path
        new_path = tmp_path + ext
        result = client.images.edit(
            model="gpt-image-1",
            image=[
                #pass the path to the model
                open(new_path, "rb"),
            ],
            prompt="Turn the drawing into a photorealistic image"
        )

    image_base64 = result.data[0].b64_json
    image_bytes = base64.b64decode(image_base64)

    # Save the image to a file
    output_img =  anvil.BlobMedia(content_type="text/jpeg", content=image_bytes, name="ai-image.jpg")
    task_id = anvil.server.context.background_task_id
    row = app_tables.tasks.add_row(image=output_img, task_id=task_id)
    return row
```

Now that our server code is finished, we can launch the background task from the `generate_button`.

Switch back to the Design view of MainForm and select the `generate_button`. Click `on click event` from the Object Palette to set up [a function that will run](https://anvil.works/docs/client/events?utm_source=crosspost:dev.to:/learn/tutorials/open-ai) when the Button is clicked.

From this function, we want to call `launch_bg_task`, passing in the uploaded file. We also want to disable the `generate_button` while the background task is running:

``` python
    @handle("generate_button", "click")
    def generate_button_click(self, **event_args):
        """This method is called when the button is clicked"""
        self.task = anvil.server.call('launch_bg_task', self.file_loader_1.file)
        self.generate_button.enabled = False
```

Switch back to Design view, and add a [LinearProgressIndicator component](https://anvil.works/docs/ui/app-themes/material-3/components#linearprogressindicator) underneath the Image components. We'll use this to indicate to the user that the image is being generated. 

Click on the eye icon from the Object Palette to make the component invisible to start. We'll make it visible while the image is being generated.

Update the `generate_button_click` event to make the `linear_progress_indicator` visible when clicked:

``` python
    @handle("generate_button", "click")
    def generate_button_click(self, **event_args):
        """This method is called when the button is clicked"""
        self.task = anvil.server.call('launch_bg_task', self.file_loader_1.file)
        self.generate_button.enabled = False
        #indicate that the task is running
        self.linear_progress_indicator_1.visible = True
```

We need to poll the server to check if the background task has finished running and our image has been generated.

Drag and drop a [Timer component](https://anvil.works/docs/ui/components/basic#timer) onto the Form. It will appear at the top because it's an invisible component. Timers have an `interval` property that determines how frequently they raise a `tick` event. We can then write a function that runs every time the Timer "ticks".

From the Properties Panel, set the Timer's `interval` to 0. We don't want it to start ticking until we tell it to. 

From the Object Palette, set up a `tick` event handler for the Timer. In here, we want to check if the background task has finished, and if so, we'll update the UI accordingly. We can also use `with anvil.server.no_loading_indicator` to stop Anvil's loading spinner from appearing every time we poll the server.

Your `timer_1_tick` function should look like this:

``` python
    @handle("timer_1", "tick")
    def timer_1_tick(self, **event_args):
        """This method is called Every [interval] seconds. Does not trigger if [interval] is 0."""
        with anvil.server.no_loading_indicator:
            #check if the background task has finished
            if self.task.is_completed():
                #get the image from the Data Table row 
                self.generated_img = self.task.get_return_value()['image']
                #display the generated image
                self.output_img.source = self.generated_image
                self.linear_progress_indicator_1.visible = False
                self.output_img.visible = True
                self.generate_button.enabled = True
                #stop the timer from ticking
                self.timer_1.interval = 0
```

Finally, we can add the ability to download the generated image.

Back in Design view, add a Button to the page and name it `download_button`. Centre align the Button and make it invisible. Change it's `appearance` to `tonal` and set its icon to `mi:downlaod`.

Set up a `click` event handler for the `download_button`, and inside that function, call `anvil.media.download(self.generated_image)`.

``` python
    @handle("download_button", "click")
    def download_button_click(self, **event_args):
        """This method is called when the component is clicked."""
        anvil.media.download(self.generated_img)
```

At the top of the Form code, make sure to import `anvil.media`:

``` python
import anvil.media
```

It's now time to test out the app! Click the green Run button, upload a drawing and click "Turn into photo". Wait for the AI to generate a photorealistic image and then try downloading it.

To publish your app to the web, click "Publish" at the top right of the editor, then "Publish this app". You'll get a URL you can share with others.

We built a complete web app that:

We did it all in Python and deployed it instantly to the web!

There's a lot you can do to extend this app:

If you're new here, welcome! [Anvil](https://anvil.works?utm_source=crosspost:dev.to:/learn/tutorials/open-ai) is a platform for building full-stack web apps with nothing but Python. No need to wrestle with JS, HTML, CSS, Python, SQL and all their frameworks - just **build it all in Python**.

Yes - Python that [runs in the browser](https://anvil.works/docs/client/python?utm_source=crosspost:dev.to:/learn/tutorials/open-ai). Python that [runs on the server](https://anvil.works/docs/server?utm_source=crosspost:dev.to:/learn/tutorials/open-ai). Python that [builds your UI](https://anvil.works/docs/client?utm_source=crosspost:dev.to:/learn/tutorials/open-ai). A [drag-and-drop UI editor](https://anvil.works/docs/client/adding-ui-elements?utm_source=crosspost:dev.to:/learn/tutorials/open-ai). We even have a built-in [Python database](https://anvil.works/docs/data-tables?utm_source=crosspost:dev.to:/learn/tutorials/open-ai), in case you don't have your own.

Why not have a play with the app builder? **It's free!** Click here to get started:
