{"slug": "building-an-ai-image-generator-with-openai", "title": "Building an AI Image Generator with OpenAI", "summary": "A developer has published a tutorial on building a web application that uses OpenAI's Images API to turn drawings into photorealistic images. The app is built with Python and Anvil, and the tutorial walks through creating the UI and integrating the API.", "body_md": "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.\n\nWhen 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.\n\nTo build the app, we will:\n\nFor this tutorial, you will need basic Python knowledge and an OpenAI account with API credits.\n\nIf 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)\n\nLet's get started!\n\n[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'.\n\nIf you can't find the New M3 theme, you may need to open the \"Advanced\" dropdown\n\nFirst, 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\".\n\nWe'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.\n\nLet'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.\n\nWe 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.\n\nCentre-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/*`.\n\nWe 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`.\n\nWe 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`.\n\nAdd another Image component next to the first one to display our generated image. Name it `output_img` and make it invisible too.\n\nWe 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.\n\nLet's write some code that will make the `uploaded_img` and `generate_button` components visible when an image is uploaded.\n\nSelect 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.\n\nAdd the following code:\n\n```\n@handle(\"file_loader_1\", \"change\")\ndef self.file_loader_1_change(self, file, **event_args):\n    \"\"\"This method is called when a new file is loaded into this FileLoader\"\"\"\n    if file:\n        self.uploaded_img.source = file\n        self.uploaded_img.visible = True\n        self.generate_button.visible = True\n```\n\nLet'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.\n\nWe'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.\n\nIf you don't already have an OpenAI account, create one at [platform.openai.com](https://platform.openai.com).\n\nOnce 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.\n\nYou'll need to verify your account to use the Images API. In the \"General\" tab, look for a \"Verify Organization\" button.\n\nIf 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\".\n\nNavigate 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.\n\nWe 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\".\n\nClick \"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.\n\nWe can now set up our backend to call the API and get a generated image.\n\n`openai` pacakage\nFirst, we need to install the OpenAI Python package in our app's server environment.\n\nIn 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.\n\nBack 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.\n\nLet'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:\n\n```\nclient = OpenAI(api_key=anvil.secrets.get_secret('OPEN_AI_API_KEY'))\n```\n\nWe'll need to modify this code so that it works in Anvil but for now, your server code should look like this:\n\n``` python\nimport anvil.secrets\nimport anvil.server\nimport base64\nfrom openai import OpenAI\nclient = OpenAI(api_key=anvil.secrets.get_secret('OPEN_AI_API_KEY'))\n\nprompt = \"\"\"\nGenerate a photorealistic image of a gift basket on a white background \nlabeled 'Relax & Unwind' with a ribbon and handwriting-like font, \ncontaining all the items in the reference pictures.\n\"\"\"\n\nresult = client.images.edit(\n    model=\"gpt-image-1\",\n    image=[\n        open(\"body-lotion.png\", \"rb\"),\n        open(\"bath-bomb.png\", \"rb\"),\n        open(\"incense-kit.png\", \"rb\"),\n        open(\"soap.png\", \"rb\"),\n    ],\n    prompt=prompt\n)\n\nimage_base64 = result.data[0].b64_json\nimage_bytes = base64.b64decode(image_base64)\n```\n\nAn 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.\n\nCreate 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:\n\n``` python\nimport anvil.secrets\nimport anvil.server\nimport base64\nfrom openai import OpenAI\nclient = OpenAI(api_key=anvil.secrets.get_secret('OPEN_AI_API_KEY'))\n\ndef generate_image(input_img):\n    result = client.images.edit(\n        model=\"gpt-image-1\",\n        image=[\n            open(\"body-lotion.png\", \"rb\"),\n            open(\"bath-bomb.png\", \"rb\"),\n            open(\"incense-kit.png\", \"rb\"),\n            open(\"soap.png\", \"rb\"),\n        ],\n        prompt=\"Turn the drawing into a photorealistic image\"\n    )\n\n    image_base64 = result.data[0].b64_json\n    image_bytes = base64.b64decode(image_base64)\n\n    # Save the image to a file\n    with open(\"gift-basket.png\", \"wb\") as f:\n        f.write(image_bytes)\n```\n\nWe 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). \n\nOpenAI 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.\n\nAdd the following import statements to your server code:\n\n``` python\nimport anvil.media\nimport mimetypes\nimport os\n```\n\nThen, 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:\n\n``` python\ndef generate_image(input_img):\n    #get the MIME type and extension from the input_img\n    mime_type = input_img.content_type\n    ext = mimetypes.guess_extension(mime_type)\n    #create a temporary file path\n    with anvil.media.TempFile(input_img) as tmp_path:\n        #add the extension to the temporary path\n        new_path = tmp_path + ext\n        result = client.images.edit(\n            model=\"gpt-image-1\",\n            image=[\n                #pass the path to the model\n                open(new_path, \"rb\"),\n            ],\n            prompt=\"Turn the drawing into a photorealistic image\"\n        )\n\n        image_base64 = result.data[0].b64_json\n        image_bytes = base64.b64decode(image_base64)\n\n        # Save the image to a file\n        with open(\"gift-basket.png\", \"wb\") as f:\n            f.write(image_bytes)\n```\n\nWhen 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:\n\n```\noutput_img =  anvil.BlobMedia(content_type=\"text/jpeg\", content=image_bytes, name=\"ai-image.jpg\")\n```\n\nThe `name` argument will be the name of your file when downloaded. You can change this name to anything you'd like.\n\nYour server code should now look like this:\n\n``` python\nimport anvil.secrets\nimport anvil.server\nimport base64\nfrom openai import OpenAI\nimport anvil.media\nimport mimetypes\nimport os\n\nclient = OpenAI(api_key=anvil.secrets.get_secret('OPEN_AI_API_KEY'))\n\ndef generate_image(input_img):\n    #get the MIME type and extension from the input_img\n    mime_type = input_img.content_type\n    ext = mimetypes.guess_extension(mime_type)\n    #create a temporary file path\n    with anvil.media.TempFile(input_img) as tmp_path:\n        #add the extension to the temporary path\n        new_path = tmp_path + ext\n        result = client.images.edit(\n            model=\"gpt-image-1\",\n            image=[\n                #pass the path to the model\n                open(new_path, \"rb\"),\n            ],\n            prompt=\"Turn the drawing into a photorealistic image\"\n        )\n\n    image_base64 = result.data[0].b64_json\n    image_bytes = base64.b64decode(image_base64)\n\n    # Save the image to a file\n    output_img =  anvil.BlobMedia(content_type=\"text/jpeg\", content=image_bytes, name=\"ai-image.jpg\")\n```\n\nThe 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).\n\nTo turn the function into a background task, we just need to decorate it with `@anvil.server.background_task`.\n\n``` python\n@anvil.server.background_task\ndef generate_image(input_img):\n    ...\n```\n\nWe 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. \n\nIn the ServerModule, add the following function\n\n``` python\n@anvil.server.callable\ndef launch_bg_task(input_img):\n    task = anvil.server.launch_background_task('generate_image', input_img)\n    return task\n```\n\nThe `@anvil.server.callable` decorator makes this function callable from our frontend code. \n\n`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.\n\n[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.\n\nChoose 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:\n\n`image` (Media column) - for the generated image`task_id` (Text column) - for the background task ID\nWhen 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:\n\n```\ntask_id = anvil.server.context.background_task_id\nrow = app_tables.tasks.add_row(image=output_img, task_id=task_id)\nreturn row\n```\n\nYour finished ServerModule should now look something like this:\n\n``` python\nimport anvil.secrets\nimport anvil.server\nimport base64\nfrom openai import OpenAI\nimport anvil.media\nimport mimetypes\nimport os\n\nclient = OpenAI(api_key=anvil.secrets.get_secret('OPEN_AI_API_KEY'))\n\n@anvil.server.callable\ndef launch_bg_task(input_img):\n    task = anvil.server.launch_background_task('generate_image', input_img)\n    return task\n\n@anvil.server.background_task\ndef generate_image(input_img):\n    #get the MIME type and extension from the input_img\n    mime_type = input_img.content_type\n    ext = mimetypes.guess_extension(mime_type)\n    #create a temporary file path\n    with anvil.media.TempFile(input_img) as tmp_path:\n        #add the extension to the temporary path\n        new_path = tmp_path + ext\n        result = client.images.edit(\n            model=\"gpt-image-1\",\n            image=[\n                #pass the path to the model\n                open(new_path, \"rb\"),\n            ],\n            prompt=\"Turn the drawing into a photorealistic image\"\n        )\n\n    image_base64 = result.data[0].b64_json\n    image_bytes = base64.b64decode(image_base64)\n\n    # Save the image to a file\n    output_img =  anvil.BlobMedia(content_type=\"text/jpeg\", content=image_bytes, name=\"ai-image.jpg\")\n    task_id = anvil.server.context.background_task_id\n    row = app_tables.tasks.add_row(image=output_img, task_id=task_id)\n    return row\n```\n\nNow that our server code is finished, we can launch the background task from the `generate_button`.\n\nSwitch 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.\n\nFrom 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:\n\n``` python\n    @handle(\"generate_button\", \"click\")\n    def generate_button_click(self, **event_args):\n        \"\"\"This method is called when the button is clicked\"\"\"\n        self.task = anvil.server.call('launch_bg_task', self.file_loader_1.file)\n        self.generate_button.enabled = False\n```\n\nSwitch 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. \n\nClick 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.\n\nUpdate the `generate_button_click` event to make the `linear_progress_indicator` visible when clicked:\n\n``` python\n    @handle(\"generate_button\", \"click\")\n    def generate_button_click(self, **event_args):\n        \"\"\"This method is called when the button is clicked\"\"\"\n        self.task = anvil.server.call('launch_bg_task', self.file_loader_1.file)\n        self.generate_button.enabled = False\n        #indicate that the task is running\n        self.linear_progress_indicator_1.visible = True\n```\n\nWe need to poll the server to check if the background task has finished running and our image has been generated.\n\nDrag 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\".\n\nFrom the Properties Panel, set the Timer's `interval` to 0. We don't want it to start ticking until we tell it to. \n\nFrom 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.\n\nYour `timer_1_tick` function should look like this:\n\n``` python\n    @handle(\"timer_1\", \"tick\")\n    def timer_1_tick(self, **event_args):\n        \"\"\"This method is called Every [interval] seconds. Does not trigger if [interval] is 0.\"\"\"\n        with anvil.server.no_loading_indicator:\n            #check if the background task has finished\n            if self.task.is_completed():\n                #get the image from the Data Table row \n                self.generated_img = self.task.get_return_value()['image']\n                #display the generated image\n                self.output_img.source = self.generated_image\n                self.linear_progress_indicator_1.visible = False\n                self.output_img.visible = True\n                self.generate_button.enabled = True\n                #stop the timer from ticking\n                self.timer_1.interval = 0\n```\n\nFinally, we can add the ability to download the generated image.\n\nBack 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`.\n\nSet up a `click` event handler for the `download_button`, and inside that function, call `anvil.media.download(self.generated_image)`.\n\n``` python\n    @handle(\"download_button\", \"click\")\n    def download_button_click(self, **event_args):\n        \"\"\"This method is called when the component is clicked.\"\"\"\n        anvil.media.download(self.generated_img)\n```\n\nAt the top of the Form code, make sure to import `anvil.media`:\n\n``` python\nimport anvil.media\n```\n\nIt'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.\n\nTo 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.\n\nWe built a complete web app that:\n\nWe did it all in Python and deployed it instantly to the web!\n\nThere's a lot you can do to extend this app:\n\nIf 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**.\n\nYes - 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.\n\nWhy not have a play with the app builder? **It's free!** Click here to get started:", "url": "https://wpnews.pro/news/building-an-ai-image-generator-with-openai", "canonical_source": "https://dev.to/anvil/building-an-ai-image-generator-with-openai-5541", "published_at": "2026-09-09 15:59:56+00:00", "updated_at": "2026-09-09 16:20:07.010031+00:00", "lang": "en", "topics": ["developer-tools", "generative-ai"], "entities": ["OpenAI", "Anvil"], "alternates": {"html": "https://wpnews.pro/news/building-an-ai-image-generator-with-openai", "markdown": "https://wpnews.pro/news/building-an-ai-image-generator-with-openai.md", "text": "https://wpnews.pro/news/building-an-ai-image-generator-with-openai.txt", "jsonld": "https://wpnews.pro/news/building-an-ai-image-generator-with-openai.jsonld"}}