>FFmpegLab
FFmpegLab Guide

AI Upscaling Pipeline – SQL Triggers, DNN & Supabase Storage

Build an automated AI upscaling pipeline that uses DNN models to upscale videos 2x, 3x, or 4x using SQL triggers, pgmq, and Supabase Storage.

AI upscaling is one of the most exciting applications of deep learning in video processing. Using FFmpeg's DNN (Deep Neural Network) filters, you can upscale low-resolution videos to 2x, 3x, or 4x their original resolution with remarkable quality — restoring detail, reducing artifacts, and breathing new life into old footage.

This guide shows you how to build a fully automated AI upscaling pipeline that turns a low‑resolution video into a high‑quality upscaled masterpiece — all driven by PostgreSQL triggers and pgmq.

Key takeaways

The Gap: From Low Resolution to High Quality

Low‑resolution footage is everywhere: old home videos, legacy content, game captures, and low‑bitrate streams. Traditional upscaling (bicubic, lanczos) simply stretches pixels, creating blurry, artifact‑ridden results. AI upscaling uses deep learning to actually reconstruct missing detail, producing sharp, natural-looking results.

But AI upscaling is computationally intensive and time‑consuming. Doing it manually for every video is impractical. What if the pipeline could be fully automated — triggered by the upload itself, processing in the background, and delivering a high‑quality upscaled video when complete?

This guide shows you exactly how to build that pipeline.

Architecture Overview

User Uploads Low-Res Video Supabase Storage PostgreSQL Trigger pgmq Queue ffmpeglab-runner
Upscaled Video Public Folder pg_notify User Notified

The pipeline consists of:

Important: This pipeline uses the existing render and logpiece tables from the FFmpegLab server. It does not create new tables — it only adds the pipeline components.

What the Pipeline Delivers

OutputFormatLocation
Upscaled VideoMP4 (H.264)public-processed/{userId}/upscaled/
Real‑time notificationspg_notify channelsN/A
Job trackingrender tableExisting FFmpegLab table
Logslogpiece tableExisting FFmpegLab table

Prerequisites

Quick Start – The Setup Script

The fastest way to set up the pipeline is to run the SQL script.

Step 1
Save the SQL script
Copy the complete SQL script from the section below and save it as setup_upscaling_pipeline.sql.
Step 2
Run the script
Execute the script against your Supabase database.
# Via psql psql -U postgres -d your_database -f setup_upscaling_pipeline.sql

# Or via the Supabase SQL Editor # Copy and paste the entire script into the SQL Editor and run it.
Step 3
Download DNN models
Download the SRCNN models for upscaling.
# Create models directory mkdir -p models/sr

# Download SRCNN model files wget -O models/sr/srcnn.xml https://github.com/guoyejun/ffmpeg_dnn/raw/main/models/openvino/2021.1/super_resolution.xml wget -O models/sr/srcnn.bin https://github.com/guoyejun/ffmpeg_dnn/raw/main/models/openvino/2021.1/super_resolution.bin
Step 4
Configure the runner
Add the upscaling queue to your runner's environment.
# Add to your .env or docker-compose.yml UPSCALING_QUEUE_NAME=upscaling_queue

# DNN model path DNN_MODEL_PATH=/app/models/sr/srcnn.xml

# DNN backend (openvino, tensorflow, native, torch) DNN_BACKEND=openvino

# Upscaling factor (2, 3, 4) UPSCALE_FACTOR=2
Step 5
Restart the runner
Restart the runner to pick up the new queue.
docker compose restart ffmpeglab-runner

That's it! The pipeline is now live. Users can upload low‑resolution videos to private-uploads/{userId}/, and they will be automatically upscaled.

The Complete SQL Setup Script

Note: This script assumes that the render and logpiece tables already exist (from the FFmpegLab server migrations). It only adds the pipeline components.

-- ============================================================
-- AI UPSCALING PIPELINE
-- Complete SQL Setup Script
-- ============================================================
-- This script adds the AI upscaling pipeline components.
-- It assumes the render and logpiece tables already exist.
-- ============================================================

-- ============================================================
-- 1. Create storage buckets
-- ============================================================
INSERT INTO storage.buckets (id, name, public, avif_autodetection, file_size_limit, allowed_mime_types)
VALUES
('private-uploads', 'private-uploads', false, false, 5368709120, ARRAY['video/mp4', 'video/quicktime', 'video/x-msvideo', 'video/webm', 'video/mpeg']),
('public-processed', 'public-processed', true, false, 5368709120, ARRAY['video/mp4'])
ON CONFLICT (id) DO NOTHING;

-- ============================================================
-- 2. RLS policies for private-uploads bucket
-- ============================================================

DROP POLICY IF EXISTS "Users can upload to their own folder" ON storage.objects;
CREATE POLICY "Users can upload to their own folder"
ON storage.objects
FOR INSERT
TO authenticated
WITH CHECK (
bucket_id = 'private-uploads' AND
(storage.foldername(name))[1] = auth.uid()::text
);

DROP POLICY IF EXISTS "Users can download from their own folder" ON storage.objects;
CREATE POLICY "Users can download from their own folder"
ON storage.objects
FOR SELECT
TO authenticated
USING (
bucket_id = 'private-uploads' AND
(storage.foldername(name))[1] = auth.uid()::text
);

DROP POLICY IF EXISTS "Users can update their own files" ON storage.objects;
CREATE POLICY "Users can update their own files"
ON storage.objects
FOR UPDATE
TO authenticated
USING (
bucket_id = 'private-uploads' AND
(storage.foldername(name))[1] = auth.uid()::text
);

DROP POLICY IF EXISTS "Users can delete their own files" ON storage.objects;
CREATE POLICY "Users can delete their own files"
ON storage.objects
FOR DELETE
TO authenticated
USING (
bucket_id = 'private-uploads' AND
(storage.foldername(name))[1] = auth.uid()::text
);

-- ============================================================
-- 3. RLS policies for public-processed bucket
-- ============================================================

DROP POLICY IF EXISTS "Public read access to processed media" ON storage.objects;
CREATE POLICY "Public read access to processed media"
ON storage.objects
FOR SELECT
USING (bucket_id = 'public-processed');

DROP POLICY IF EXISTS "Service role can manage processed media" ON storage.objects;
CREATE POLICY "Service role can manage processed media"
ON storage.objects
FOR ALL
TO service_role
USING (bucket_id = 'public-processed');

DROP POLICY IF EXISTS "Users can read their own processed media" ON storage.objects;
CREATE POLICY "Users can read their own processed media"
ON storage.objects
FOR SELECT
TO authenticated
USING (
bucket_id = 'public-processed' AND
(storage.foldername(name))[1] = auth.uid()::text
);

-- ============================================================
-- 4. Enable pgmq extension and create the queue
-- ============================================================
CREATE EXTENSION IF NOT EXISTS pgmq;

SELECT pgmq.create('upscaling_queue');

-- ============================================================
-- 5. Create the trigger function for video uploads
-- This function builds the exact FFmpeg commands to be executed by the runner.
-- ============================================================
DROP FUNCTION IF EXISTS handle_video_upscale() CASCADE;

CREATE OR REPLACE FUNCTION handle_video_upscale()
RETURNS TRIGGER AS $$
DECLARE
user_id text;
file_path text;
file_name text;
file_extension text;
mime_type text;
base_filename text;
msg jsonb;
commands jsonb := '[]';
video_mime_types text[] := ARRAY['video/mp4', 'video/quicktime', 'video/x-msvideo', 'video/webm', 'video/mpeg'];
upscale_factor text := '2'; -- Default: 2x upscaling
dnn_backend text := 'openvino'; -- Default backend
model_path text := '/app/models/sr/srcnn.xml'; -- SRCNN model path
BEGIN
-- Only process video files in the private-uploads bucket
IF NEW.bucket_id = 'private-uploads' AND NEW.metadata->>'mimetype' = ANY(video_mime_types) THEN
user_id := (storage.foldername(NEW.name))[1];
file_path := NEW.name;
file_name := (storage.filename(NEW.name));
file_extension := split_part(file_name, '.', array_length(string_to_array(file_name, '.'), 1));
mime_type := NEW.metadata->>'mimetype';
base_filename := replace(file_name, '.' || file_extension, '');

-- Build the upscaling command
-- Option 1: Direct DNN upscaling with SRCNN (2x)
commands := commands || jsonb_build_object(
'type', 'upscale',
'scaleFactor', upscale_factor,
'backend', dnn_backend,
'output_path', user_id || '/upscaled/' || base_filename || '_' || upscale_factor || 'x.mp4',
'command', 'ffmpeg -i "INPUT_FILE" -vf "format=rgb24,dnn_processing=model=' || model_path || ':input=x:output=y:dnn_backend=' || dnn_backend || ',scale=iw*' || upscale_factor || ':ih*' || upscale_factor || '" -c:v libx264 -crf 18 -pix_fmt yuv420p "OUTPUT_FILE"'
);

-- Option 2: Simple bicubic upscaling + sharpening (fallback)
commands := commands || jsonb_build_object(
'type', 'upscale',
'scaleFactor', upscale_factor,
'backend', 'bicubic',
'output_path', user_id || '/upscaled/' || base_filename || '_' || upscale_factor || 'x_bicubic.mp4',
'command', 'ffmpeg -i "INPUT_FILE" -vf "scale=iw*' || upscale_factor || ':ih*' || upscale_factor || ':flags=lanczos,unsharp=5:5:1.5:5:5:0.5" -c:v libx264 -crf 18 -pix_fmt yuv420p "OUTPUT_FILE"'
);

-- Build the job message
msg := jsonb_build_object(
'userId', user_id,
'inputPath', file_path,
'inputBucket', NEW.bucket_id,
'outputBucket', 'public-processed',
'fileName', file_name,
'baseFilename', base_filename,
'mimeType', mime_type,
'scaleFactor', upscale_factor,
'commands', commands,
'timestamp', NOW()
);

-- Push to pgmq queue
PERFORM pgmq.send('upscaling_queue', msg::jsonb);

-- Insert into render table for job tracking
INSERT INTO "render" (id, title, project, status, public, user_id, data)
VALUES (
gen_random_uuid(),
file_name,
'upscaling',
'queued',
false,
user_id::uuid,
msg
);

-- Notify via pg_notify
PERFORM pg_notify(
'upscaling_upload_channel',
jsonb_build_object(
'userId', user_id,
'filePath', file_path,
'fileName', file_name,
'status', 'queued',
'timestamp', NOW()
)::text
);
END IF;

RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- ============================================================
-- 6. Attach the trigger to storage.objects
-- ============================================================
DROP TRIGGER IF EXISTS upscaling_upload_trigger ON storage.objects;

CREATE TRIGGER upscaling_upload_trigger
AFTER INSERT ON storage.objects
FOR EACH ROW
EXECUTE FUNCTION handle_video_upscale();

-- ============================================================
-- 7. Helper views for monitoring
-- ============================================================

DROP VIEW IF EXISTS upscaling_queue_view;

CREATE OR REPLACE VIEW upscaling_queue_view AS
SELECT
msg_id,
read_ct,
enqueued_at,
vt,
message::jsonb as payload,
(message::jsonb->>'userId') as user_id,
(message::jsonb->>'fileName') as file_name,
(message::jsonb->>'scaleFactor') as scale_factor
FROM pgmq.q_upscaling_queue
ORDER BY msg_id DESC;

-- ============================================================
-- 8. Initialize notification channels
-- ============================================================
DO $$
BEGIN
PERFORM pg_notify('upscaling_upload_channel', '{"init": true}');
PERFORM pg_notify('render_status_channel', '{"init": true}');
PERFORM pg_notify('log_channel', '{"init": true}');
END $$;

-- ============================================================
-- All done!
-- ============================================================

What the Script Does

ComponentDescription
Storage BucketsCreates private-uploads (private) and public-processed (public) buckets with file size limits and allowed MIME types
RLS PoliciesSets per‑user isolation for private uploads and public read access for processed media
pgmq QueueCreates the upscaling_queue for job processing
Trigger Functionhandle_video_upscale() — fires on new uploads, builds FFmpeg commands for DNN upscaling, pushes to pgmq, inserts into render, sends notifications
ViewsHelper views for monitoring queue status
Uses Existing TablesUses render for job tracking and logpiece for logs (from FFmpegLab server)

Processing Logic Explained

When a video is uploaded to private-uploads/{userId}/, the pipeline:

  1. Identifies the video — checks if the file is a video.
  2. Builds two FFmpeg commands — one for AI upscaling (DNN) and one for bicubic upscaling (fallback).
  3. Creates a render job in the existing render table with the commands in the data column.
  4. Pushes a job to the upscaling_queue with the commands payload.
  5. Sends a notification via pg_notify.
  6. The ffmpeglab-runner picks up the job, resolves the INPUT_FILE and OUTPUT_FILE placeholders, and executes the commands.

Exact FFmpeg Commands

The trigger function generates the following FFmpeg commands using placeholders:

1. AI Upscaling with DNN (SRCNN)

ffmpeg -i "INPUT_FILE" -vf "format=rgb24,dnn_processing=model=/app/models/sr/srcnn.xml:input=x:output=y:dnn_backend=openvino,scale=iw*2:ih*2" -c:v libx264 -crf 18 -pix_fmt yuv420p "OUTPUT_FILE"
💡
DNN Upscaling Parameters Explained
  • format=rgb24 — DNN models typically expect RGB24 format.
  • dnn_processing — FFmpeg's DNN processing filter.
  • model=/app/models/sr/srcnn.xml — Path to the SRCNN model (OpenVINO format).
  • input=x:output=y — Input and output tensor names for the model.
  • dnn_backend=openvino — DNN backend (openvino, tensorflow, native, torch).
  • scale=iw*2:ih*2 — Upscale the DNN output to the target size.
  • -crf 18 — High quality encoding for the upscaled result.

2. Bicubic Upscaling (Fallback)

ffmpeg -i "INPUT_FILE" -vf "scale=iw*2:ih*2:flags=lanczos,unsharp=5:5:1.5:5:5:0.5" -c:v libx264 -crf 18 -pix_fmt yuv420p "OUTPUT_FILE"
💡
Bicubic Upscaling Parameters Explained
  • scale=iw*2:ih*2:flags=lanczos — Lanczos scaling (highest quality).
  • unsharp=5:5:1.5:5:5:0.5 — Unsharp mask to restore sharpness after scaling.
  • -crf 18 — High quality encoding.

FFmpeg Command Table (Quick Reference)

OperationFFmpeg Command
2x AI Upscaling (SRCNN)ffmpeg -i input.mp4 -vf "format=rgb24,dnn_processing=model=srcnn.xml:input=x:output=y:dnn_backend=openvino,scale=iw*2:ih*2" -c:v libx264 -crf 18 output.mp4
3x AI Upscaling (SRCNN)ffmpeg -i input.mp4 -vf "format=rgb24,dnn_processing=model=srcnn.xml:input=x:output=y:dnn_backend=openvino,scale=iw*3:ih*3" -c:v libx264 -crf 18 output.mp4
4x AI Upscaling (SRCNN)ffmpeg -i input.mp4 -vf "format=rgb24,dnn_processing=model=srcnn.xml:input=x:output=y:dnn_backend=openvino,scale=iw*4:ih*4" -c:v libx264 -crf 18 output.mp4
Bicubic Upscaling (Fallback)ffmpeg -i input.mp4 -vf "scale=iw*2:ih*2:flags=lanczos,unsharp=5:5:1.5:5:5:0.5" -c:v libx264 -crf 18 output.mp4

Downloading DNN Models

The pipeline uses SRCNN (Super-Resolution Convolutional Neural Network) models. You can download them from the FFmpeg DNN model repository.

Step 1
Download SRCNN models
Download the model files for your chosen backend.
# OpenVINO format (recommended for Intel CPUs) wget -O srcnn.xml https://github.com/guoyejun/ffmpeg_dnn/raw/main/models/openvino/2021.1/super_resolution.xml wget -O srcnn.bin https://github.com/guoyejun/ffmpeg_dnn/raw/main/models/openvino/2021.1/super_resolution.bin

# TensorFlow format wget -O srcnn.pb https://github.com/guoyejun/ffmpeg_dnn/raw/main/models/tensorflow/srcnn.pb
Step 2
Place models in the runner
Copy the models to the runner's model directory.
# Create models directory in the runner mkdir -p /app/models/sr

# Copy models (adjust paths as needed) cp srcnn.xml /app/models/sr/ cp srcnn.bin /app/models/sr/
Step 3
Verify model files
List the model files to confirm they are present.
ls -la /app/models/sr/ # Should show srcnn.xml and srcnn.bin

Configure ffmpeglab-runner

The runner needs to be configured to poll the upscaling_queue, mount the model files, and execute the provided FFmpeg commands.

Step 1
Add the queue to your environment
Add the following to your .env file or Docker Compose configuration.
# Upscaling queue UPSCALING_QUEUE_NAME=upscaling_queue

# DNN model path DNN_MODEL_PATH=/app/models/sr/srcnn.xml

# DNN backend DNN_BACKEND=openvino
Step 2
Mount models in Docker Compose
Add a volume mount for the models directory.
# In docker-compose.yml services: ffmpeglab-runner: volumes: - ./models:/app/models
Step 3
Implement the processing loop
The runner should execute the following steps for each job:
# 1. Connect to Supabase and listen to the queue

# 2. For each job:
# a. Download the input file from private-uploads
# b. Parse the 'commands' array from the job payload
# c. For each command:
# - Replace 'INPUT_FILE' with the local input path
# - Replace 'OUTPUT_FILE' with a temporary local path
# - Execute the FFmpeg command
# - Upload the output file to public-processed/{output_path}
# - Update the render table with progress and logs
# d. Mark the job as complete in the render table
# e. Delete the job from the queue
Step 4
Install FFmpeg with DNN support
FFmpeg must be compiled with DNN support for your chosen backend.
# For OpenVINO: ./configure --enable-libopenvino make && make install

# For TensorFlow: ./configure --enable-libtensorflow make && make install
Step 5
Restart the runner
After updating the environment, restart the runner service.
docker compose restart ffmpeglab-runner

Monitor the Pipeline

You can monitor the pipeline using SQL queries and notifications.

Step 1
Check queued jobs
Use the helper view to see all queued jobs.
SELECT * FROM upscaling_queue_view;
Step 2
Check render status
Query the existing render table for job status.
SELECT id, title, status, progress, data
FROM "render"
WHERE project = 'upscaling'
ORDER BY created_at DESC;
Step 3
Listen to notifications
In your application, listen for real‑time updates.
-- In your PostgreSQL client:
LISTEN upscaling_upload_channel;
LISTEN render_status_channel;
LISTEN log_channel;
Step 4
Check processed files
List all processed files in the public bucket.
SELECT name, metadata, created_at FROM storage.objects
WHERE bucket_id = 'public-processed'
ORDER BY created_at DESC;

Frequently Asked Questions (FAQ)

What models are used for AI upscaling?

The pipeline uses FFmpeg's dnn_processing filter with SRCNN (Super-Resolution Convolutional Neural Network) models. SRCNN is a lightweight model that works well for 2x upscaling. For higher upscaling factors, you can use ESPCN or other models.

What upscaling factors are supported?

The pipeline supports 2x, 3x, and 4x upscaling. The default is 2x upscaling using the SRCNN model. You can configure the scale factor in the trigger function or by modifying the FFmpeg command.

What DNN backends are supported?

FFmpeg supports TensorFlow, OpenVINO, Torch, and Native DNN backends. The pipeline uses OpenVINO by default as it provides the best performance for Intel CPUs and GPUs. You can change the backend in the dnn_processing filter.

Is this pipeline suitable for real-time upscaling?

No. DNN-based upscaling is computationally intensive. A 5-minute video can take 1-2 hours to upscale, depending on the resolution and hardware. This pipeline is designed for batch processing where time is not critical.

How do I improve upscaling quality?

To improve quality, you can: (1) Use a larger model like EDSR or Real-ESRGAN (requires custom model conversion), (2) Increase the bitrate (-b:v) or use -crf 14, (3) Use the unsharp filter after upscaling to restore sharpness.

Final Word

You now have a fully automated AI upscaling pipeline that uses deep learning to transform low‑resolution videos into high‑quality upscaled content. With PostgreSQL triggers, pgmq, and Supabase Storage, you get:

The pipeline is production‑ready, scalable, and configurable. It uses the existing render and logpiece tables from the FFmpegLab server, so there are no table conflicts — just pure, AI-powered upscaling.

Important: DNN upscaling is computationally intensive. Start with small test clips and monitor your runner's CPU usage before scaling up to larger videos.