Build a Django AI colorization app 🎨🦾

We'll build a simple Django app that uses AI to color black and white photos 🎨

Here's how the final app looks:


I've also made a simple video guide (featuring me 🙂) that follows the below instructions. Here's the video:

How our AI colorization app will work

  1. Our Django server will send a request to an external function (e.g., AI function)
  2. The external function will run for a long time (i.e., anything that's a long time in server terms. For me, this is over 1 second)
  3. The webhook: The external function sends a request to our Django server when it's finished
  4. Our Django server updates the database with the result from the external function
  5. Our Django frontend shows the updated result

I'll guide you through the easiest method to use webhooks in Django. Webhooks are a neat way of external functions (e.g., AI functions) sending data to your server 🪝

Let's start building 🚀

0. Setup your Django app

  • Install Django and create a project and app:
pip install django python-dotenv replicate
django-admin startproject core .
python manage.py startapp sim
  • Add sim to INSTALLED_APPS in core/settings.py:
INSTALLED_APPS = [
    ... # Other apps
    'sim',
]

1. Run ngrok

Q. How can external functions send webhooks to our local development server?

Issue: The external function can't send a webhook to your local server. Your local server is not exposed to the internet.

Solution: We'll create a route from the internet to your local server (known as a tunnel). This is easy once you know how to do it. We'll use ngrok for this.

Install ngrok

It's pretty simple. There are 2 commands to run here (P.S I'm unaffiliated with ngrok. It's just good).

Start ngrok

Start Ngrok on a particular port by running the below in your terminal.

ngrok http 8000

This will expose port 8000 (default Django port) to the internet.

Ngrok will provide a forwarding URL (e.g., https://12345.ngrok.io), which is the address that connects to your local server. On the free version, the url changes everytime your restart ngrok.

2. Create your Replicate account

  • Visit https://replicate.com/ and create an account.
  • Create a new API token. Go to Account settings > API tokens > Create token ss1.png

3. Add your webhook url and Replicate API token to your environment variables

  • Create a file called .env at "core/.env" and add the below to it. We'll use this to store our environment variables, which we won't commit to version control.
WEBHOOK_URL=<your_ngrok_url>
REPLICATE_API_TOKEN=<your_api_token>
  • Add the following to the top of core/settings.py. This will load the environment variables from the .env file when the server restarts (i.e., when you run python manage.py runserver).
```python
from pathlib import Path
import os
from dotenv import load_dotenv


load_dotenv()

if not os.environ['REPLICATE_API_TOKEN']:
    raise Exception('REPLICATE_API_TOKEN not set. Have you added it to your .env file at "core/.env" ?')

if not os.environ['WEBHOOK_URL']:
    raise Exception('WEBHOOK_URL not set. Have you added it to your .env file at "core/.env" ?')
  • In "core/settings.py", add your to "ALLOWED_HOSTS", as well as adding localhost and 127.0.0.1:
ALLOWED_HOSTS = [
    'localhost',
    '127.0.0.1',
    <your_ngrok_url>,  # Be sure to exclude the https://
]

Note, if you restart ngrok, the free version of ngrok means you'll need to update the references to your ngrok url with the new ngrok url.

4. Create your Django app

We'll now create Django app with an endpoint to which the external function (the webhook) will send requests.

Create models

  • Create a model in "sim/models.py" to store the results of the external function. This will be the database that the external function updates.
from django.db import models


class Generation(models.Model):
    secret_key = models.CharField(max_length=100, blank=False, null=True)
    created_at = models.DateTimeField(auto_now_add=True)
    before_url = models.URLField(blank=False, null=True)
    after_url = models.URLField(blank=False, null=True)
    status = models.CharField(max_length=20, default="created")

    def __str__(self):
        return f"Generation {self.id} | {self.status}"

  • Create your database:
python manage.py makemigrations
python manage.py migrate

Create views

Q. How can we ignore unauthorized requests to our webhook endpoint?

Issue: We don't want anyone to be able to send a request to our webhook. We only want the external function to be able to send a request to our webhook.

Solution: We'll use a secret key that only the external function knows.

Add your views

  • Add the below to "sim/views.py".
import json
import os
import uuid

from django.http import JsonResponse, HttpResponse
from django.shortcuts import render
from django.urls import reverse
from django.views.decorators.csrf import csrf_exempt
from .models import Generation
import replicate


def generations(request, *args) -> HttpResponse:
    """
    Render all generations.dj runserver
    """
    generations = Generation.objects.all().order_by('-created_at')
    return render(
        request, 'generations.html', {'generations': generations}
    )


def start_generation(request) -> JsonResponse:
    """
    We call this view to start a new generation.
    """
    image_url = request.POST['image_url']

    secret_id = uuid.uuid4()  # We'll use this to prevent people from sending us fake webhooks.
    generation = Generation.objects.create(secret_key=secret_id, before_url=image_url, status="started")

    uri = reverse('complete-generation', kwargs={'secret_key': secret_id})
    webhook_url = f"{os.environ['WEBHOOK_URL']}{uri}"

    replicate_model_id = "piddnad/ddcolor:ca494ba129e44e45f661d6ece83c4c98a9a7c774309beca01429b58fce8aa695"
    replicate.run(
        replicate_model_id,
        input={"image": image_url, "model_size": "large"},
        webhook=webhook_url,
        webhook_events_filter=["completed"],
    )
    return JsonResponse({"generation_id": generation.id}, status=200)


def check_generation(request, generation_id: int) -> JsonResponse:
    """
    We use this to poll the status of the generation, and then update the UI.
    """
    generation = Generation.objects.get(id=generation_id)
    return JsonResponse({"status": generation.status}, status=200)


@csrf_exempt
def complete_generation(request, secret_key: int) -> HttpResponse:
    """
    The external webhook will call this endpoint when the generation is complete.

    See the external webhook docs for Replicate below:
    For general use: https://replicate.com/docs/webhooks
    For Python: https://github.com/replicate/replicate-python?tab=readme-ov-file#run-a-model-in-the-background-and-get-a-webhook
    """
    if request.method == 'POST':
        webhook_data = json.loads(request.body.decode("utf"))
        print(f'{webhook_data = }')
        output_image_url = webhook_data['output']

        try:
            generation = Generation.objects.get(secret_key=secret_key)
            generation.after_url = output_image_url
            generation.status = 'completed'
            generation.save()
            return HttpResponse(status=200)
        except Generation.DoesNotExist:
            return HttpResponse(status=404)
    else:
        return HttpResponse(status=403)

Create templates

  • Create templates to render the views

  • Create a folder called "templates" at "sim/templates"

  • Add a file called "generations.html" to your templates folder:
<head>
    <meta charset="utf-8" content="width=device-width, initial-scale=1" name="viewport"/>
    <title>Your generations</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
            background-color: #f4f4f4;
            color: #333;
            line-height: 1.6;
        }

        .container {
            width: 80%;
            margin: auto;
            overflow: hidden;
        }

        .header {
            padding: 20px 0;
            text-align: center;
        }

        .image-row {
            display: flex;
            justify-content: center;
            align-items: center;
            margin: 20px 0;
            gap: 10px;
        }

        .image-container {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }

        .image {
            cursor: pointer;
            width: 100%;
            max-width: 250px;
            max-height: 250px;
            border-radius: 10px;
            box-shadow: 0 5px 15px rgba(0,0,0,0.1);
            margin: 10px 0;
        }

        .form {
            margin: auto;
            width: 50%;
            background: #fff;
            padding: 20px;
            box-shadow: 0 5px 15px rgba(0,0,0,0.1);
            border-radius: 10px;
        }
        .form-header {
            text-align: center;
            margin-bottom: 20px;
        }

        label {
            display: block;
            margin-bottom: 5px;
        }

        input[type='url'], button {
            width: 100%;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 5px;
        }

        button {
            background: #333;
            color: #fff;
            border: 0;
            padding: 10px 15px;
            margin-top: 10px;
            cursor: pointer;
        }

        button:hover {
            background: #555;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="header">
        <h1>Your Generations</h1>
    </div>

    <form @submit.prevent="submit" class="form" data-component-name="PageForm" id="page-form-b430f14e-879f-423b-80cc-59d9f7035685"
          x-cloak="" x-data="{ status: 'normal', errors: {}, image_url: 'https://static.demilked.com/wp-content/uploads/2016/07/unseen-rare-historical-photos-12.jpg'}">
        <div class="form-header">
            Image to Colorise 🎨
        </div>
        <div data-component-name="PageFormState" id="page-form-state-605eeca3-f9ac-48f5-98f2-59fe7d7ce06f" x-show="status === 'normal'">

            <div class="image-container" target="_blank">
                <a href="{{ generation.before_url }}" target="_blank">
                <img x-show="image_url" class="image" :src="image_url"  alt="">
                    </a>
            </div>
            <input data-component-name="PageFormInput" id="page-form-input-78e2b175-a83c-49a4-b4b1-3ee9ed192de0"
                   name="image_url" x-model="image_url" type="url"/>
            <button data-component-name="PageFormSubmitButton" id="page-form-submit-button-cf673914-855b-43c1-a53e-f1c06434e2fa">
                Submit
            </button>
        </div>

        <div data-component-name="PageFormState" id="page-form-state-2cf868eb-2da0-4ca5-be65-9f13ca2be920" x-show="status === 'success'">
            <div data-component-name="PageText" id="page-text-fe45a37e-86c4-4786-adda-58cd50abd461">
                Successfully submitted form ✅
            </div>
        </div>
        <div data-component-name="PageFormState" id="page-form-state-a8b68f4b-8ae3-49db-a235-2db5a3f65bd7" x-show="status === 'error'">
            <div data-component-name="PageText" id="page-text-cadbfdfd-c1c9-496f-8b70-3f496108a95b">
                Error submitting your form ❌
            </div>
        </div>

        <script>
            function submit(event) {
                /*
                Submit the form to the server to start the generation.
                 */
                event.preventDefault();
                const formData = new FormData(event.target);
                const object = Object.fromEntries(formData);
                const payload = JSON.stringify(object);

                fetch("/start-generation", {
                    method: "post",
                    body: formData,
                    headers: {
                        'X-CSRFToken': '{{ csrf_token }}',
                    },
                })
                    .then(response => {
                        this.status = response.ok ? 'success' : 'error';
                        return response.json();
                    })
                    .then(data => {
                        this.errors = data.errors || {};
                        if (data.generation_id) {
                            this.generationId = data.generation_id;
                            poll(this.generationId);
                        }
                        else{
                            this.status = 'error';
                        }
                    });
            }

            function poll(generationId) {
                /*
                Poll the server to check if the generation is complete.
                 */
                let attempts = 30;
                const intervalId = setInterval(() => {
                    if (attempts <= 0) {
                        clearInterval(intervalId);
                        this.status = 'error';
                        return;
                    }
                    fetch(`/check-generation/${generationId}`)
                        .then(response => response.json())
                        .then(data => {
                            if (data.status === 'completed') {
                                window.location.reload();
                            }
                        });
                    attempts--;
                }, 1000);
            }
        </script>
        <script defer="" src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js">
        </script>
    </form>


    {% for generation in generations %}
    <div class="image-row">
        <div class="image-container" >
            <div>Before</div>
            <a href="{{ generation.before_url }}" target="_blank">
                <img class="image" src="{{ generation.before_url }}">
            </a>
        </div>
        <div class="image-container" href="{{ generation.after_url }}" target="_blank">
            <div>After</div>
            <a href="{{ generation.after_url }}" target="_blank">
                <img class="image" src="{{ generation.after_url }}">
            </a>
        </div>
    </div>
    {% endfor %}
    {% if generations|length == 0 %}
    <div>
        <span>You have no generations yet.</span>
    </div>
    {% endif %}
</div>
</body>

Create urls

  • Create urls to map the views to urls

  • core/urls.py:

from django.contrib import admin
from django.urls import path, include


urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('sim.urls')),
]

  • Create a file at sim/urls.py and add:
from django.urls import path
from . import views


urlpatterns = [
    path('', views.generations, name='generations'),
    path('start-generation', views.start_generation, name='start-generation'),
    path('check-generation/<int:generation_id>', views.check_generation, name='check-generation'),
    path('complete-generation/<str:secret_key>', views.complete_generation, name='complete-generation'),
]

Run your server

  • Run your server:
python manage.py runserver

Remember to make your that your ngrok is running at the same time in a separate terminal window.

  • Visit your app at http://localhost:8000

Another view of the finished app:


Congrats 🎉

You've built a Django app that:

  • colors black and white photos using AI
  • receives the results neatly using webhooks, and
  • displays the results in the browser

Here are a few samples of the AI colorization that I got from the app:

Example 1 of AI colorization using our Django app Example 2 of AI colorization using our Django app Example 3 of AI colorization using our Django app

P.S Want to build your Django frontend even faster? 🚀

Probably like you, I want to make my Django product ideas become reality as soon as possible.

That's why I built Photon Designer - a new way of building Django frontend as fast as possible, and entirely visually.

Photon Designer outputs neat, clean Django templates.

Let's get visual.

Do you want to create beautiful django frontends effortlessly?
Click below to book your spot on our early access mailing list (as well as early adopter prices).