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
- One SQL script — everything is set up with a single execution.
- Zero manual intervention — upload a low-res video, get a high-res upscaled version.
- AI-powered upscaling — uses SRCNN and other DNN models for super‑resolution.
- Scalable and reliable — pgmq provides durable, transaction‑safe job queuing.
- Configurable — choose between 2x, 3x, and 4x upscaling factors.
- Multiple backends — supports TensorFlow, OpenVINO, Torch, and Native backends.
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
Upscaled Video → Public Folder → pg_notify → User Notified
The pipeline consists of:
- Supabase Storage — two buckets:
private-uploads(per‑user) andpublic-processed(per‑user, with subfolders for upscaled videos). - RLS policies — restrict access to each user's own folders.
- PostgreSQL trigger — fires on
INSERTintostorage.objects. - pgmq — message queue for job processing.
- ffmpeglab-runner — executes FFmpeg commands with DNN upscaling.
- pg_notify — real‑time status updates.
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
| Output | Format | Location |
|---|---|---|
| Upscaled Video | MP4 (H.264) | public-processed/{userId}/upscaled/ |
| Real‑time notifications | pg_notify channels | N/A |
| Job tracking | render table | Existing FFmpegLab table |
| Logs | logpiece table | Existing FFmpegLab table |
Prerequisites
- A Supabase project (cloud or self‑hosted).
- ffmpeglab-server and ffmpeglab-runner deployed (see setup guide).
- The
renderandlogpiecetables must already exist (created by the FFmpegLab server migrations). - FFmpeg compiled with DNN support (
--enable-libopenvinoor--enable-libtensorflow). - DNN model files downloaded (see Downloading DNN Models).
- Access to your Supabase database (psql or the Supabase SQL Editor).
Quick Start – The Setup Script
The fastest way to set up the pipeline is to run the SQL script.
setup_upscaling_pipeline.sql.# Or via the Supabase SQL Editor # Copy and paste the entire script into the SQL Editor and run it.
# 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
# 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
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
| Component | Description |
|---|---|
| Storage Buckets | Creates private-uploads (private) and public-processed (public) buckets with file size limits and allowed MIME types |
| RLS Policies | Sets per‑user isolation for private uploads and public read access for processed media |
| pgmq Queue | Creates the upscaling_queue for job processing |
| Trigger Function | handle_video_upscale() — fires on new uploads, builds FFmpeg commands for DNN upscaling, pushes to pgmq, inserts into render, sends notifications |
| Views | Helper views for monitoring queue status |
| Uses Existing Tables | Uses 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:
- Identifies the video — checks if the file is a video.
- Builds two FFmpeg commands — one for AI upscaling (DNN) and one for bicubic upscaling (fallback).
- Creates a render job in the existing
rendertable with the commands in thedatacolumn. - Pushes a job to the
upscaling_queuewith the commands payload. - Sends a notification via
pg_notify. - The ffmpeglab-runner picks up the job, resolves the
INPUT_FILEandOUTPUT_FILEplaceholders, and executes the commands.
Exact FFmpeg Commands
The trigger function generates the following FFmpeg commands using placeholders:
INPUT_FILE— The path to the downloaded input file (resolved by the runner).OUTPUT_FILE— The temporary path for the output file (resolved by the runner).
1. AI Upscaling with DNN (SRCNN)
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)
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)
| Operation | FFmpeg 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.
# TensorFlow format wget -O srcnn.pb https://github.com/guoyejun/ffmpeg_dnn/raw/main/models/tensorflow/srcnn.pb
# Copy models (adjust paths as needed) cp srcnn.xml /app/models/sr/ cp srcnn.bin /app/models/sr/
Configure ffmpeglab-runner
The runner needs to be configured to poll the upscaling_queue, mount the model files, and execute the provided FFmpeg commands.
.env file or Docker Compose configuration.# DNN model path DNN_MODEL_PATH=/app/models/sr/srcnn.xml
# DNN backend DNN_BACKEND=openvino
# 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
# For TensorFlow: ./configure --enable-libtensorflow make && make install
Monitor the Pipeline
You can monitor the pipeline using SQL queries and notifications.
render table for job status.FROM "render"
WHERE project = 'upscaling'
ORDER BY created_at DESC;
LISTEN upscaling_upload_channel;
LISTEN render_status_channel;
LISTEN log_channel;
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:
- AI-powered upscaling — SRCNN models for super‑resolution
- Multiple upscaling factors — 2x, 3x, or 4x
- Multiple DNN backends — TensorFlow, OpenVINO, Torch, Native
- Real‑time notifications — know when processing is complete
- Full observability — monitoring views and logs
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.