Build a Powerful Video Processing Pipeline with AssemblyAI and Deploy it to Koyeb
Tags: ai, webdev, tutorial, django
Published: 2024-05-24 13:55:37

In the digital era, video is the king, demanding innovative solutions for efficient processing and distribution.

This guide introduces a powerful approach using Koyeb's cloud services to build a scalable video processing pipeline. We will rely on secure protocols for video uploads, employ AI-driven tagging and classification via AssemblyAI, and leverage Koyeb's built-in CDN technology for global content distribution. Embrace the power of serverless architecture to meet the growing demand for video content, ensuring optimal performance and viewer satisfaction.

You can follow along with this guide by viewing the GitHub repositories for the [video web app](https://github.com/koyeb/example-video-processing-app) and the [video worker service](https://github.com/koyeb/example-video-processing-worker).

## Requirements

Before diving into building your video processing pipeline with Koyeb, it's important to ensure that you have the necessary tools and knowledge. This section outlines the prerequisites needed to follow the upcoming guide successfully.

- A [Koyeb account](https://app.koyeb.com/auth/signup) will be required for deploying. It will be helpful to have a foundational understanding of its service offerings (web service and database, in this case).
- An [AssemblyAI API key](https://www.assemblyai.com/dashboard/signup) to integrate AI-driven video tagging and classification capabilities. **Note:** You will need to add credit to your account to use the LLM features implemented in this guide.
- Knowledge of Python programming for scripting and automation within the serverless architecture.
- Experience with Django for developing robust, scalable web applications that interfaces effectively with back-end services.

## Steps

1. [Set up the database](#set-up-the-database): This step involves setting up a database service on Koyeb to store and manage video metadata. This database will be used by the web application to store and access data.
2. [Build the web application](#build-the-web-application): This section guides you through developing a simple web application for video uploads along with features to view and search videos. The application is built with Django.
3. [Build the worker service](#build-the-worker-service): This step covers implementing a service API that processes the video uploads. The API will incorporate AI technologies from AssemblyAI for tagging, classifying, and potentially transcoding videos. Additionally, the API will utilize Koyeb's autoscaling features to efficiently manage varying video processing loads.
4. [Integrate with Koyeb's edge network](#integrate-with-koyebs-edge-network): This section details how to integrate the web application with Koyeb's edge network to enhance the distribution of video content globally.

## Set up the database

Setting up the database is an important part of this process. This database will be used to keep and organize video metadata, such as video titles, descriptions, lengths, file types, and other important information.

For this article you are going to setup a PostgreSQL database using Koyeb's recent fully-managed serverless PostgreSQL databases feature.

Here's a step-by-step guide on how to set up the database on Koyeb:

1. In the [Koyeb control panel](https://app.koyeb.com/), click **Create Database Service**.
2. Choose alternatives to or confirm the provided defaults for the name and role fields.
3. Choose the region closest to you or your users.
4. Select the database size. If you are not already using it, you can deploy the **Free** tire as the Instance type.
5. Click **Create Database Service**.

Once the database is created, access the database's detail page and there you can check the connection details.

Since you will be using Django later, select **Django** and copy the database connection details. Store this locally somewhere so that you can reference it later. It will look like something like this:

```python
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'koyebdb',
        'USER': 'koyeb-adm',
        'PASSWORD': <YOUR_DB_PASSWORD>,
        'HOST': '<YOUR_DB_URL>.eu-central-1.pg.koyeb.app',
        'OPTIONS': {'sslmode': 'require'},
    }
}
```

Once you've completed these steps, your database will be ready to store and manage video metadata for your application.

## Build the web application

This part of the guide will show you how to make a simple web application using Django and deploy it to Koyeb. This app will let users upload videos and also provide features to watch for these uploaded videos.

The focus of this article is the end-to-end process, so in this section we will only highlight the code relevant to this process. You can check the full source code in the [project's GitHub repository](https://github.com/koyeb/example-video-processing-pipeline).

### Create a virtual environment and initialize a new project

To get started, create a project directory and then initialize a new virtual environment inside by typing:

```bash
mkdir example-video-app
cd example-video-app
python3 -m venv venv
```

Activate the new virtual environment by typing:

```bash
source venv/bin/activate
```

Install Django within the virtual environment. We'll also install some additional libraries and packages that we'll use in the application while we're here:

```bash
pip install Django requests python-decouple psycopg2-binary Pillow
```

Save the project's dependencies to a `requirements.txt` file by typing:

```bash
pip freeze > requirements.txt
```

With Django installed, create a new Django project called **VideoApp** rooted in the existing project directory (be sure to include the trailing dot to avoid creating an extra directory hierarchy):

```bash
django-admin startproject VideoApp .
```

Next, create a new Django application called **App** that the project will incorporate:

```bash
python manage.py startapp App
```

This will create a new `App` directory alongside the existing `VideoApp` directory.

### Set up the app models

In the `App/models.py` file, you can now define the Django models for the video metadata. Replace the current contents with the following:

```python
# File: App/models.py
from django.db import models


# Model to store video metadata
class Video(models.Model):
    title = models.CharField(max_length=100)
    description = models.TextField()
    video_file = models.FileField(upload_to='videos/')
    uploaded_at = models.DateTimeField(auto_now_add=True)
    duration = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
    size = models.CharField(max_length=10, null=True, blank=True)

    def __str__(self):
        return self.title


# Model to store video related tags
class Tag(models.Model):
    name = models.CharField(max_length=50)
    video = models.ManyToManyField(Video, related_name='tags')

    def __str__(self):
        return self.name


# Model to store video classification
class Category(models.Model):
    name = models.CharField(max_length=50)
    video = models.ManyToManyField(Video, related_name='categories')

    def __str__(self):
        return self.name
```

Here you are defining three database models: `Video`, `Tag`, and `Category`.

The `Video` model is used to store video metadata, including the title, description, video file, upload date and time, duration, and size.

The `Tag` model is used to store video-related tags. It has a many-to-many relationship with the `Video` model, meaning that a video can have multiple tags and a tag can be associated with multiple videos. The `related_name` attribute specifies the name of the reverse relation from the `Video` model back to the `Tag` model.

The `Category` model is used to store video classifications. It also has a many-to-many relationship with the `Video` model, meaning that a video can belong to multiple categories and a category can contain multiple videos. The `related_name` attribute specifies the name of the reverse relation from the `Video` model back to the `Category` model.

## Create a video upload form

In order to be able to upload any videos, you need to have a form defined in Django, let's now create that in `App/forms.py`:

```python
# File: App/forms.py
from django import forms
from .models import Video, Tag, Category


# Form for video upload
class VideoForm(forms.ModelForm):
    class Meta:
        model = Video
        fields = ['title', 'description', 'video_file']
```

The `VideoForm` is a `ModelForm`, a special form created from a Django model. In this case, the `Video` model is used.

The `Meta` class inside `VideoForm` is used to specify additional metadata for the form. The `model` attribute indicates which Django model this form is associated with, and the `fields` attribute is a list of model fields that should be included in the form. In this case, the form includes fields for `title`, `description`, `video_file`.

The `tags` and `categories`, as well as `duration` and `size` will be filled in later on with the results from the worker service API.

This form will allow users to input data for these fields, which will then be saved as a new `Video` object in the database when the form is submitted.

### Configure the App views

With the form in place, next you can create the view that will handle the form submission. Replace the contents of `App/views.py` with the following:

```python
# File: App/views.py
import requests
from decouple import config
from django.shortcuts import render
from App.forms import VideoForm
from App.models import Tag, Category, Video


# View to handle video upload from the video upload form
def upload_video(request):
    form = VideoForm()
    if request.method == 'POST':
        form = VideoForm(request.POST, request.FILES)
        if form.is_valid():
            # Save the video
            form.save()
            # Process the video file with the worker
            video_url = config("DOMAIN") + form.instance.video_file.url
            worker_url = config("WORKER_URL") + "/process_video?video_url=" + video_url
            # Send a GET request to the worker URL
            response = requests.get(worker_url)
            if response.status_code == 200:
                # Get the response data
                response_data = response.json()
                # Save the video tags
                tags = response_data.get('tags')
                for tag in tags:
                    tag, created = Tag.objects.get_or_create(name=tag)
                    form.instance.tags.add(tag)
                # Save the video categories
                categories = response_data.get('categories')
                for category in categories:
                    category, created = Category.objects.get_or_create(name=category)
                    form.instance.categories.add(category)
                # Save the video duration and resolution
                form.instance.duration = response_data.get('duration')
                form.instance.size = response_data.get('resolution')
                form.instance.save()
            # Return a success message
            return render(request, 'upload_video.html',
                          {'form': form, 'message': 'Video uploaded successfully!'})
        else:
            # Return the form with errors
            return render(request, 'upload_video.html', {'form': form,
                                                         'message': 'Error uploading video!'})
    return render(request, 'upload_video.html', {'form': form})


def list_videos(request):
    videos = Video.objects.all()
    return render(request, 'list_videos.html', {'videos': videos})
```

The `upload_video` view function handles the video upload process. When a user submits the video upload form, this function is called to process the form data. It starts by creating an instance of the `VideoForm` with the form data and files. If the form is valid, it saves the video, processes the video file with a worker service, and saves the video metadata (tags, categories, duration, and resolution) returned by the worker service API. If the form is not valid, it returns the form with errors. If the request method is not POST, it simply renders the video upload form.

The `list_videos` view function retrieves all videos from the database and renders them in a template.

### Create the application templates

In order for this view to work, you will need to create the HTML template. Create a new directory for HTML templates:

```bash
mkdir App/templates
```

Inside, create a new file at `App/templates/upload_video.html` with the following content:

```html
<!-- File: App/templates/upload_video.html -->
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Video App</title>
  </head>
  <body>
    <h1>Video App</h1>

    <p><a href="{% url 'list_videos' %}">Back to Video list</a></p>

    <h3>Upload your video</h3>
    <form method="post" enctype="multipart/form-data">
      {% csrf_token %} {{ form.as_p }}
      <button type="submit">Upload</button>
    </form>

    {{ message }}
  </body>
</html>
```

This HTML markup creates a simple web page for uploading videos.

The video upload form is created using the HTML `<form>` tag. The `method` attribute is set to `post`, which means the form data will be sent to the server using the HTTP `POST` method. The `enctype` attribute is set to "multipart/form-data", which is necessary for forms that allow file uploads. The `{% csrf_token %}` template tag is used to protect against cross-site request forgery attacks.

When the form is submitted, the data is sent to the server and handled by the `upload_video` view function in the `views.py` file. As mentioned earlier, if the form is valid, the video is saved and processed by the worker service, and a success message is displayed. If the form is not valid, an error message is displayed.

You will also need an HTML template to list the videos. Add the following to a `App/templates/list_videos.html` file:

```html
<!-- File: App/templates/list_videos.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Video App</title>
</head>
<body>

    <h1>Video App</h1>

    <p><a href="{% url 'upload_video' %}">Upload New Video</a></p>

    {% for video in videos %}

        <video controls width='50%' height='50%'>
            <source src="{{ video.video_file.url }}" type="video/mp4"></source>
        </video>
        <h2>{{ video.title }}</h2>
        <p>Description: {{ video.description }}</p>
        <p>Tags:
            {% for tag in video.tags.all %}
                {{ tag.name }},
            {% endfor %}
        </p>
        <p>Categories:
            {% for category in video.categories.all %}
                {{ category.name }},
            {% endfor %}
        </p>
        <p>Duration: {{ video.duration }} seconds</p>
        <p>Resolution: {{ video.size }}</p>

        <p>-----</p>
    {% endfor %}

</body>
</html>
```

The `{% for video in videos %}` template tag is used to loop through the list of videos passed from the `list_videos` view function in the `views.py` file. For each video, it displays the video player, title, description, tags, categories, duration, and resolution.

The `<video>` element embeds a video player in the web page. The `controls` attribute is used to display the video controls, such as play, pause, and volume. The `width` and `height` attributes are used to set the size of the video player. The `<source>` element is used to specify the video file and its MIME type.

The `{{ video.title }}`, `{{ video.description }}`, `{{ video.duration }}`, and `{{ video.size }}` template tags are used to display the video metadata.

The `{% for tag in video.tags.all %}` and `{% for category in video.categories.all %}` template tags are used to loop through the list of tags and categories associated with the video. The `{{ tag.name }}` and `{{ category.name }}` template tags are used to display the name of each tag and category.

When the web page is loaded, the `list_videos` view function in the `views.py` file is called to retrieve the list of videos from the database and pass it to the template. The template then loops through the list of videos and displays them on the web page.

### Define the URL routing

To access the views and HTML templates, you need to define the URLs, which you can add to `App/urls.py`:

```python
# File: App/urls.py
from django.urls import path
from App import views


urlpatterns = [
    path('', views.list_videos, name='list_videos'),
    path('upload', views.upload_video, name='upload_video'),
]
```

The `urlpatterns` list contains two `path` objects that map URLs to view functions. The first `path` object maps the root URL ('') to the `list_videos` view function and assigns it the name 'list_videos'. This means that when a user navigates to the `/` URL of the application, the `list_videos` view function will be called to handle the request and render the appropriate response.

The second `path` object maps the 'upload' URL to the `upload_video` view function and assigns it the name 'upload_video'. This means that when a user navigates to the `/upload` URL, the `upload_video` view function will be called to handle the request and render the appropriate response.

Hook the new `App/urls.py` file in to the project URL processing by editing the `VideoApp/urls.py` file as follows:

{/* prettier-ignore-start */}

```python
# File: VideoApp/urls.py
from django.contrib import admin
from django.urls import path  # [!code --]
from django.urls import include, path  # [!code ++]
from django.conf import settings  # [!code ++]
from django.conf.urls.static import static  # [!code ++]

urlpatterns = [
    path('', include("App.urls")),  # [!code ++]
    path('admin/', admin.site.urls),
]

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)  # [!code ++]
```

{/* prettier-ignore-end */}

### Adjust the project configuration

Hook the application up with the project by editing the `VideoApp/settings.py` file and adding the `AppConfig` instance declared in the `App/apps.py` file to the list of `INSTALLED_APPS`:

{/* prettier-ignore-start */}

```python
# File: VideoApp/settings.py
. . .

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'App.apps.AppConfig',  # [!code ++]
]

. . .
```

{/* prettier-ignore-end */}

You also need to make sure that we connect the Django project to the PostgreSQL database defined earlier. Also in the `VideoApp/settings.py` file, edit the `DATABASES` dictionary to take parameterized values from environment variables:

```python
# File: VideoApp/settings.py
. . .

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': config("DJANGO_DB_NAME"),
        'USER': config("DJANGO_DB_USER"),
        'PASSWORD': config("DJANGO_DB_PASSWORD"),
        'HOST': config("DJANGO_DB_HOST"),
        'OPTIONS': {'sslmode': 'require'},
    }
}

. . .
```

We'll finish the configuration by setting some required variables and configuring static files. First, add some new imports to the top of the file:

{/* prettier-ignore-start */}

```python
# File: VideoApp/settigns.py
import os  # [!code ++]
from pathlib import Path

from decouple import config  # [!code ++]
. . .
```

{/* prettier-ignore-end */}

Next, create or set the following variables:

```python
# File: VideoApp/settings.py
. . .

SECRET_KEY = config("DJANGO_SECRET_KEY")
DEBUG = True

ALLOWED_HOSTS = config("ALLOWED_HOSTS", cast=lambda v: [s.strip() for s in v.split(',')])
DOMAIN = config("DOMAIN")
CSRF_TRUSTED_ORIGINS = config("CSRF_TRUSTED_ORIGINS", cast=lambda v: [s.strip() for s in v.split(',')])

. . .
```

The `ALLOWED_HOSTS` variable is used to set a list of allowed host names for the application. This is a security feature that prevents host header attacks.

The `DOMAIN` variable is used to set the domain name of the application. This is used in the `upload_video` view function to construct the URL for the worker service.

The `CSRF_TRUSTED_ORIGINS` variable is used to set a list of trusted origins for the Cross-Site Request Forgery (CSRF) protection in Django. This is a security feature that prevents malicious websites from making unauthorized requests to the application.

Finally, configure the static file configuration by adding the following:

```python
# File: VideoApp/settings.py
. . .

STATIC_ROOT = os.path.join(BASE_DIR, 'static')
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

. . .
```

Now, you can create and run the migrations, with:

```bash
python manage.py makemigrations
python manage.py migrate
```

The main page and the video upload form are now complete and should render if you run the test server with the expected environment variables configured. To actually upload videos, however, you need to create the associated worker service.

To finish up with the web app, create a [new repository on GitHub](https://github.com/new). Afterwards, initialize a git repository in the project root, download a basic Python `.gitignore` file, and push the changes:

```bash
git init
curl -L https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore -o .gitignore
echo "videos" >> .gitingore
git add :/
git commit -m "Initial commit"
git remote add origin git@github.com:<YOUR_GITHUB_USERNAME>/<YOUR_GITHUB_REPOSITORY>.git
git branch -M main
git push -u origin main
```

## Build the worker service

You will build a second application with FastAPI that will be responsible for processing uploads.

Outside of the Django project directory, create a new project directory for the worker API service. Deactivate any existing virtual environments and create a new virtual environment:

```bash
deactivate  # If you're not currently in a virtual environment, this command will fail. This is expected.
mkdir example-video-worker
cd example-video-worker
python3 -m venv venv
```

Activate the new virtual environment by typing:

```bash
source venv/bin/activate
```

Create a `requirements.txt` file with the service's dependencies:

```
# File: requirements.txt
fastapi
assemblyai
python-decouple
requests
moviepy
uvicorn
```

Install the dependencies by typing:

```bash
pip install -r requirements.txt
```

The important packages installed here are `assemblyai`, which is a speech recognition and natural language processing API, and `moviepy`, which is a library for video editing and processing.

Create a `.env` file to define your AssemblyAI API key as an environment variable:

```bash
# File: .env
ASSEMBLYAI_API_KEY=<YOUR_ASSEMBLYAI_API_KEY>
```

You will need an API key from [AssemblyAI](https://www.assemblyai.com/), which you can get [here]([AssemblyAI | Dashboard](https://www.assemblyai.com/app/)). You will need to sign up for an account if you don't have one. To use the AI features (like LEMUR) you will need to add credits to the account.

Then you can create your `main.py` file inside the FastAPI project:

```python
# File: main.py
import requests
from decouple import config
from fastapi import FastAPI
import assemblyai as aai
from moviepy.video.io.VideoFileClip import VideoFileClip
import os


# Set up the AssemblyAI client
aai.settings.api_key = config("ASSEMBLYAI_API_KEY")
transcriber = aai.Transcriber()


# Define a function to download a video file from a URL
def download_video(url, filename):
    # Send a GET request to the URL
    response = requests.get(url, stream=True)
    # Check if the request was successful
    if response.status_code == 200:
        # Open a local file in binary write mode
        with open(filename, 'wb') as file:
            # Write the content of the response to the file in chunks
            for chunk in response.iter_content(chunk_size=8192):
                file.write(chunk)


# Define a function to extract audio from a video file
def extract_audio_from_video(video_file_path, output_audio_path):
    # Load the video file
    video = VideoFileClip(video_file_path)
    # Extract the audio from the video
    audio = video.audio
    # Write the audio to a file
    audio.write_audiofile(output_audio_path)
    # Close the video file to free up resources
    video.close()


# Define a function to get the resolution and duration of a video file
def get_resolution_and_duration_from_video(video_file_path):
    # Load the video file
    video = VideoFileClip(video_file_path)
    # Get the resolution of the video
    resolution = video.size
    # Get the duration of the video
    duration = video.duration
    # Close the video file to free up resources
    video.close()
    return resolution, duration


# Create a FastAPI instance
app = FastAPI()


# Define a route handler for the default route, for health checks
@app.get("/")
async def version():
    return {"version": "v0.1"}


# Define a route handler for the /process_video route
@app.get("/process_video")
async def process_video(video_url: str):
    # Download the video file from the URL and save it locally
    print("Downloading video...")
    video_filename = "video.mp4"
    download_video(video_url, video_filename)

    # Get audio from video file with MoviePy
    print("Extracting audio from video...")
    audio_filename = "audio.mp3"
    extract_audio_from_video(video_filename, audio_filename)

    # Get resolution and duration of the video
    print("Getting resolution and duration...")
    resolution, duration = get_resolution_and_duration_from_video(video_filename)
    # Format the resolution as a string
    resolution = f"{resolution[0]}x{resolution[1]}"

    # Transcribe the audio with AssemblyAI
    print("Transcribing audio...")
    transcript = transcriber.transcribe(audio_filename)

    # Generate tags for the video
    print("Generating tags...")
    prompt_tags = ("Generate a list of tags (max 5) for this video."
                   "Return only the tags, separated by commas and nothing else.")
    result = transcript.lemur.task(prompt_tags)
    tags = result.response.replace("\n", " ").split(",")
    # Trim the tags
    tags = [tag.strip() for tag in tags]
    # Limit the number of tags to 5
    tags = tags[:5]

    # Generate the categories for the video
    print("Generating categories...")
    prompt_categories = ("Generate a list of categories (max 3) for this video."
                         "Return only the categories, separated by commas and nothing else.")
    result = transcript.lemur.task(prompt_categories)
    categories = result.response.replace("\n", " ").split(",")
    # Trim the categories
    categories = [category.strip() for category in categories]
    # Limit the number of categories to 3
    categories = categories[:3]

    # Delete the video and audio files
    print("Cleaning up...")
    os.remove(video_filename)
    os.remove(audio_filename)

    # Return the tags and categories
    print("Processing complete!")
    record = {"tags": tags, "categories": categories, "resolution": resolution, "duration": duration}
    print(record)
    return record
```

This script first sets up an API key for the AssemblyAI service, which is used for transcribing audio. It then defines several functions for downloading a video file from a URL, extracting audio from a video file, and getting the resolution and duration of a video file.

Afterwards, it creates a FastAPI instance and defines two route handlers. The first route handler is for the default route (`/`) and simply returns the version number of the service.

The second route handler is for the `/process_video` route and performs the following steps:

1. Downloads the video file from the provided URL and saves it locally.
2. Extracts audio from the video file using MoviePy.
3. Gets the resolution and duration of the video file.
4. Transcribes the audio using AssemblyAI.
5. Generates tags and categories for the video using AssemblyAI's LEMUR model.
6. Deletes the video and audio files from local storage.
7. Returns the generated tags, categories, resolution, and duration as a JSON object.

The `assemblyai` library is used for transcribing audio and generating tags and categories and the `moviepy` library is used for extracting audio from a video file and getting the resolution and duration of a video file.

To finish up with the worker service, create a [new repository on GitHub](https://github.com/new). Afterwards, initialize a git repository in the project root, download a basic Python `.gitignore` file, and push the changes:

```bash
git init
curl -L https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore -o .gitignore
printf "%s\n" "*.mp4" "*.mp3" >> .gitignore
git add :/
git commit -m "Initial commit"
git remote add origin git@github.com:<YOUR_GITHUB_USERNAME>/<YOUR_GITHUB_REPOSITORY>.git
git branch -M main
git push -u origin main
```

## Integrate with Koyeb's edge network

Integrating with Koyeb's edge network requires nothing more than deploying the applications to Koyeb. All CDN features are enabled by default and all services are part of the service mesh network and edge network.

Let's now see how you can deploy the applications to Koyeb to build the pipeline.

## Deploy the web application

You can start by first deploying the web application. For that go the [Koyeb control panel](https://app.koyeb.com/) and click **Create Web Service**:

1. Select **GitHub** as your deployment method and select your GitHub project for the web application.
2. In the **Builder** section, override the **Run command** with `python manage.py runserver 0.0.0.0:8000`.
3. In the **App and Service names** section, configure the **App name**. This will impact the environment variable values you define next.
4. In the **Environment variables** section, click **Bulk edit** and configure the following variables:

   ```
   DJANGO_DB_HOST=
   DJANGO_DB_USER=
   DJANGO_DB_PASSWORD=
   DJANGO_DB_NAME=
   ALLOWED_HOSTS=
   CSRF_TRUSTED_ORIGINS=
   DJANGO_SECRET_KEY=
   DOMAIN=
   WORKER_URL=
   ```

   Fill in the variables as follows:

   - `DJANGO_DB_HOST`: The hostname of the PostgreSQL database.
   - `DJANGO_DB_USER`: The PostgreSQL username to authenticate with.
   - `DJANGO_DB_PASSWORD`: The PostgreSQL password to authenticate with.
   - `DJANGO_DB_NAME`: The name of the PostgreSQL database to connect to.
   - `ALLOWED_HOSTS`: The bare hostname where this application will be deployed. It will begin with your App name followed by your Koyeb org name, a hash, and end with `.koyeb.app`.
   - `CSRF_TRUSTED_ORIGINS`: The domain where this application will be deployed. It will begin with `https://` and include your App name, Koyeb org name, a hash, and end with `.koyeb.app`.
   - `DOMAIN`: The domain where this application will be deployed. It will begin with `https://` and include your App name, Koyeb org name, a hash, and end with `.koyeb.app`.
   - `DJANGO_SECRET_KEY`: A secret key used for encryption by Django. You can follow the procedure in [generate a secure Django secret key](https://stackoverflow.com/a/57678930) locally to generate a secure Django key.
   - `WORKER_URL`: The internal URL where your service worker will be deployed. This should take the following format: `http://<WORKER_SERVICE_NAME>.<YOUR_KOYEB_ORG>.koyeb:8080`. Use the name you plan to deploy your service worker under.

5. Click **Deploy**.

After a couple of minutes the application should be deployed and accessible at the application's URL.

## Deploy the worker service API

Next, deploy the worker API service. Navigate to the previous created application in the [Koyeb control panel](https://app.koyeb.com/) and click **Create Service**:

1. Select **GitHub** as your deployment method and select your GitHub project for the worker service API.
2. In the **Builder** section, override the **Run command** with `uvicorn main:app --port 8080 --host 0.0.0.0`.
3. In the **Environment variables** section, configure the following environment variable: `ASSEMBLYAI_API_KEY=<YOUR_ASSEMBLYAI_API_KEY>`.
4. In the **Scaling** section, select **Autoscaling** from 1 to 3 Instances. Set the number of requests per second to your desired threshold.
5. In the **Exposed ports** section, deselect the **Public** toggle to make it only accessible from the service mesh and set the port to **8080**.
6. In the **App and Service names** section, set the **Service name** to the value you chose in the `WORKER_URL` variable when you deployed the Django application.
7. Click **Deploy**.

After a couple of minutes the Worker Web API should be deployed.

## Test the application

You can now test the web application and the worker API pipeline by accessing the web application URL and uploading a video file.

In this example, first we upload a file and fill in the title and description:

![Video upload form](https://www.koyeb.com/static/images/tutorials/video-processing-pipeline/video_upload_form.png)

We can observe the different steps of the video worker API in the Koyeb logs:

![Video processing logs](https://www.koyeb.com/static/images/tutorials/video-processing-pipeline/video_processing_logs.png)

And finally, returning to the web application, we can see the categories and tags as well as the additional information filled in:

![View categorized video](https://www.koyeb.com/static/images/tutorials/video-processing-pipeline/view_categorized_video.png)

You now have a full functional working video pipeline which can automatically scale when the number of requests for the video worker API crosses the threshold.

## Conclusion

This article described how to build a video processing app using FastAPI, AssemblyAI, and Django on Koyeb. We've covered everything from setting up the app and creating the FastAPI service to implementing video processing features using AssemblyAI and MoviePy.

Throughout the article, we've seen how to build a reliable and scalable app that can process videos and extract valuable metadata, such as transcriptions, tags, and categories. You can customize and extend the app to meet your specific needs and use cases.

With the skills and knowledge you've gained from this article, you can confidently create your own video processing apps and use Koyeb to deploy innovative solutions for video processing and analysis. The demand for video content and video processing is growing rapidly, so the abilities you've learned here will be extremely valuable if you want to develop advanced video processing apps.