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:
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 🪝
pip install django python-dotenv replicate
django-admin startproject core .
python manage.py startapp sim
INSTALLED_APPS = [
... # Other apps
'sim',
]
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.
It's pretty simple. There are 2 commands to run here (P.S I'm unaffiliated with ngrok. It's just good).
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.
.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>
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" ?')
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.
We'll now create Django app with an endpoint to which the external function (the webhook) will send requests.
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}"
python manage.py makemigrations
python manage.py migrate
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.
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 to render the views
Create a folder called "templates" at "sim/templates"
<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 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')),
]
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'),
]
python manage.py runserver
Remember to make your that your ngrok is running at the same time in a separate terminal window.
Another view of the finished app:
You've built a Django app that:
Here are a few samples of the AI colorization that I got from the app:
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.