A minimal Python‑SQLite clone of AutoClip in 45 minutes
Welcome to another official UVF IT guide! In this quick‑fire tutorial we’ll build a tiny version of AutoClip – an AI‑assisted video clip generator that extracts highlights from long videos using subtitles.
You’ll end up with a Flask app that accepts a video file and an optional SRT, runs a simple keyword‑density model to pick highlight timestamps, and serves the clipped MP4s – all on your local machine, no paid APIs required.
Create a fresh virtual environment so our dependencies stay isolated from any system Python packages. Then install Flask, which will serve the web UI and API, and the SQLite driver (the built‑in sqlite3 module works, but we also install pysqlite3 for a nicer API). Finally, lay out a minimal folder tree: a place for the Flask app, a models.py for the database schema, and a static folder for uploaded videos.
The scaffold gives us a clean starting point. All later code will live inside the src package, making imports straightforward and keeping the repository tidy.
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install Flask pysqlite3
mkdir -p src/static src/templates
touch src/__init__.py src/app.py src/models.pyWe need a single table to remember each generated clip: the original video filename, start and end seconds, and a short AI‑generated title. Using pysqlite3 we can create the table if it doesn’t exist and expose a tiny helper to insert new rows.
Keeping the schema in models.py means the rest of the code can import get_db() and add_clip() without worrying about raw SQL strings. This keeps the project modular and easy to extend later.
src/models.py
import os
import sqlite3
from pathlib import Path
DB_PATH = Path(__file__).parent / "clips.db"
def get_db():
"""Return a connection to the SQLite DB, creating the file if needed."""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def init_db():
"""Create the clips table if it doesn't already exist."""
with get_db() as conn:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS clips (
id INTEGER PRIMARY KEY AUTOINCREMENT,
video_filename TEXT NOT NULL,
start_sec REAL NOT NULL,
end_sec REAL NOT NULL,
title TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""
)
conn.commit()
def add_clip(video_filename: str, start_sec: float, end_sec: float, title: str) -> int:
"""Insert a new clip record and return its new ID."""
with get_db() as conn:
cur = conn.execute(
"INSERT INTO clips (video_filename, start_sec, end_sec, title) VALUES (?,?,?,?)",
(video_filename, start_sec, end_sec, title),
)
conn.commit()
return cur.lastrowid
# Initialise the DB when this module is first imported
if not DB_PATH.exists():
init_db()In this step we turn raw subtitles into actionable highlight windows. We read an optional SRT file, split it into 10‑second buckets, and count how often each word appears inside each bucket. The two buckets with the highest word‑frequency scores become our highlight candidates – a lightweight TF‑IDF‑like heuristic that works without any heavy ML libraries.
The extractor returns a list of (start_seconds, end_seconds) tuples that the rest of the pipeline can feed into ffmpeg. Keeping the logic pure Python makes it easy to test and swap out later if you want a smarter model.
extractor.py
import re
import os
from collections import Counter, defaultdict
def _load_srt(srt_path):
"""Read an SRT file and return a list of (start, end, text) tuples in seconds."""
if not os.path.isfile(srt_path):
return []
pattern = re.compile(r"(\d{2}:\d{2}:\d{2},\d{3}) --> (\d{2}:\d{2}:\d{2},\d{3})\n(.+?)(?=\n\n|\Z)", re.DOTALL)
def _ts(t):
h,m,s = t.split(':')
s,ms = s.split(',')
return int(h)*3600 + int(m)*60 + int(s) + int(ms)/1000.0
entries = []
with open(srt_path, 'r', encoding='utf-8') as f:
content = f.read().strip()
for match in pattern.finditer(content):
start = _ts(match.group(1))
end = _ts(match.group(2))
text = match.group(3).replace('\n', ' ')
entries.append((start, end, text))
return entries
def _tokenize(text):
return re.findall(r"\b\w+\b", text.lower())
def extract_highlights(video_path, srt_path=None, window=10, top_n=2):
"""Return a list of (start, end) seconds for the most "interesting" windows.
If no SRT is supplied we fall back to the first `top_n` windows of the video.
"""
# Load subtitles; if missing, create empty placeholder list
subs = _load_srt(srt_path) if srt_path else []
# Determine video duration (fallback to 60s if unknown)
try:
import subprocess, json
probe = subprocess.check_output([
'ffprobe', '-v', 'error', '-show_entries', 'format=duration',
'-of', 'json', video_path
])
duration = float(json.loads(probe)['format']['duration'])
except Exception:
duration = 60.0
# Build buckets
buckets = defaultdict(list) # start_sec -> list of words
for start, end, text in subs:
bucket_start = int(start // window) * window
buckets[bucket_start].extend(_tokenize(text))
# Score each bucket by raw word frequency
scores = []
for b_start in range(0, int(duration), window):
words = buckets.get(b_start, [])
freq = Counter(words)
score = sum(freq.values()) # simple count
scores.append((score, b_start))
# Pick top windows
top = sorted(scores, reverse=True)[:top_n]
highlights = []
for _, b_start in top:
highlights.append((b_start, min(b_start + window, duration)))
return highlightsNow we glue everything together with a tiny Flask app. The root route shows a simple HTML form where you can drop a video and an optional SRT file. When the form is submitted we store the uploads, call extract_highlights to get the interesting segments, and invoke ffmpeg to slice those segments into separate MP4 files.
Each generated clip is recorded in a SQLite database (created earlier in the scaffold) so the results page can list them with download links. The UI stays minimal – just an unordered list – but it proves the end‑to‑end flow works from upload to playback.
app.py
import os
import sqlite3
from flask import Flask, request, redirect, url_for, send_from_directory, render_template_string
from werkzeug.utils import secure_filename
from extractor import extract_highlights
import subprocess
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['CLIP_FOLDER'] = 'clips'
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
os.makedirs(app.config['CLIP_FOLDER'], exist_ok=True)
DB_PATH = 'clips.db'
# Simple DB helper
def init_db():
with sqlite3.connect(DB_PATH) as conn:
conn.execute('''CREATE TABLE IF NOT EXISTS clips (
id INTEGER PRIMARY KEY AUTOINCREMENT,
video TEXT,
start REAL,
end REAL,
clip_path TEXT
)''')
init_db()
HTML_FORM = '''
<!doctype html><title>AutoClip Clone</title>
<h1>Upload video (and optional .srt)</h1>
<form method=post enctype=multipart/form-data>
<input type=file name=video required><br><br>
<input type=file name=srt><br><br>
<input type=submit value=Upload>
</form>
{% if clips %}<h2>Generated Clips</h2><ul>{% for c in clips %}<li><a href="{{url_for('download_clip', filename=c['clip_name'])}}">Clip {{c['id']}} ({{c['start']}}‑{{c['end']}}s)</a></li>{% endfor %}</ul>{% endif %}
'''
@app.route('/', methods=['GET', 'POST'])
def upload():
clips = []
if request.method == 'POST':
video_file = request.files['video']
srt_file = request.files.get('srt')
if not video_file:
return 'No video uploaded', 400
video_name = secure_filename(video_file.filename)
video_path = os.path.join(app.config['UPLOAD_FOLDER'], video_name)
video_file.save(video_path)
srt_path = None
if srt_file and srt_file.filename:
srt_name = secure_filename(srt_file.filename)
srt_path = os.path.join(app.config['UPLOAD_FOLDER'], srt_name)
srt_file.save(srt_path)
# Extract highlight windows
highlights = extract_highlights(video_path, srt_path)
# Generate clips via ffmpeg
for start, end in highlights:
clip_name = f"clip_{int(start)}_{int(end)}.mp4"
clip_path = os.path.join(app.config['CLIP_FOLDER'], clip_name)
subprocess.run([
'ffmpeg', '-y', '-i', video_path,
'-ss', str(start), '-to', str(end),
'-c', 'copy', clip_path
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
# Store metadata
with sqlite3.connect(DB_PATH) as conn:
cur = conn.execute('INSERT INTO clips (video, start, end, clip_path) VALUES (?,?,?,?)',
(video_name, start, end, clip_name))
clip_id = cur.lastrowid
clips.append({'id': clip_id, 'clip_name': clip_name, 'start': start, 'end': end})
else:
# GET – show any existing clips for demo purposes
with sqlite3.connect(DB_PATH) as conn:
rows = conn.execute('SELECT id, clip_path, start, end FROM clips').fetchall()
clips = [{'id': r[0], 'clip_name': r[1], 'start': r[2], 'end': r[3]} for r in rows]
return render_template_string(HTML_FORM, clips=clips)
@app.route('/clips/<filename>')
def download_clip(filename):
return send_from_directory(app.config['CLIP_FOLDER'], filename, as_attachment=True)
if __name__ == '__main__':
app.run(debug=True)With the code in place, start the Flask development server and point your browser at the root URL. The upload page will appear; choose a short MP4 (and an SRT if you have one) and hit Upload. After a few seconds ffmpeg will slice the two highlight clips and the page will refresh showing download links.
You can click each link to confirm the MP4 files were created and play correctly. This completes the minimal clone – you now have a working pipeline from raw video to AI‑style highlights without any external ML services.
# Ensure dependencies are installed
pip install flask werkzeug
# ffmpeg must be on PATH – on Ubuntu you can install with:
# sudo apt-get install ffmpeg
# Start the app
python app.py| Check | Command / Why it matters |
|---|---|
| Server starts without error | Observe "Running on http://127.0.0.1:5000/" in terminal |
| Upload page loads in browser | Navigate to http://127.0.0.1:5000/ and see the upload form |
| Clips appear after processing | After submitting a video, the results page lists downloadable MP4 links |
Your minimal AutoClip clone is up and running. Experiment by tweaking the window size or swapping the TF‑IDF scorer for a small transformer model – the sky’s the limit!