Auto-Detect LoL Kill Highlights
Use OpenCV template matching to auto-clip kills from any LoL video.
Introduction
30 Second Summary
You have watched a League of Legends highlight video and thought "I just want the kills, not the 30 minutes of farming." Scrubbing through game footage manually to find every fight is tedious and slow.
In this project, you will build a local web app that takes any League of Legends YouTube video URL and automatically produces individual video clips of every kill in the game.
What You'll Build
Imagine pasting a YouTube URL, clicking one button, and watching as your app spits out downloadable MP4 clips of every kill it found.
By the end of this project, you'll have:
- A working video processing pipeline that downloads YouTube videos, scans frames for kill icons using OpenCV template matching, and cuts clips with FFmpeg.
- A Streamlit web interface where you paste a URL, adjust detection sensitivity with a slider, and download individual highlight clips as MP4 files.
- Hands-on experience with computer vision techniques you can adapt to detect any visual pattern in any video.
- Secret Mission: Add a detection preview mode that shows matched frames with bounding boxes so you can visually verify and tune your detection.
Are there any prerequisites?
You'll need basic Python knowledge (functions, loops, file I/O). No prior experience with computer vision or video processing is needed.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters to you.
Set Up Your Environment
To build a kill clipper, you need three categories of tools: a computer vision library to scan video frames, a video downloader to grab YouTube content, and a UI framework to make the tool interactive.
In this step, you will create your project folder, install the Python libraries for each of those jobs, and set up FFmpeg for cutting video clips.
In this step, get ready to:
- Create your project folder and install Python dependencies.
- Install FFmpeg for video clipping.
- Verify the full setup by launching Streamlit.
Create your project folder and install dependencies
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- Open the integrated terminal by pressing Ctrl+` (Windows) or Cmd+` (macOS).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir lol-clipper
cd lol-clipper
- Open this folder in VS Code by clicking File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the lol-clipper folder, and click Open.
VS Code reloads with lol-clipper as your workspace. The Explorer sidebar on the left shows an empty folder.
- Open a fresh terminal in the new workspace by pressing Ctrl+` (Windows) or Cmd+` (macOS).
- Create a virtual environment by running this command:
python -m venv venv
- Activate the virtual environment:
Windows
venv\Scripts\activate
macOS
source venv/bin/activate
You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.
- Install the project dependencies by running:
pip install opencv-python==4.13.0.90 yt-dlp==2026.7.4 streamlit==1.59.1
What are these libraries?
- opencv-python is the Python binding for OpenCV. You will use it to scan video frames for kill icons.
- yt-dlp downloads YouTube videos from a URL. It handles format selection and extraction automatically.
- streamlit is a Python framework for building interactive web apps. It turns your script into a browser UI with minimal code.
The install takes a minute or two. When it finishes, you should see Successfully installed followed by the package names.
Seeing a permission error or install failure?
- Make sure (venv) is showing in your terminal prompt. If it is missing, re-run the activation command above.
- If you see a network timeout, check your internet connection and try running the install command again.
Help me debug my pip install error.
Install FFmpeg
FFmpeg is a command-line tool that cuts, encodes, and transforms video files. Your app will call it behind the scenes to slice clips around each detected kill.
- Check if FFmpeg is already installed by running:
ffmpeg -version
✔️ I see a version number
FFmpeg is already on your machine. Move on to the next substep.
ⓧ Command not found
FFmpeg is not installed yet. Follow the instructions for your operating system below.
Windows
- Open your browser and go to https://www.gyan.dev/ffmpeg/builds/.
- Download the release essentials zip file (look for ffmpeg-release-essentials.zip).
- Extract the zip contents to C:\ffmpeg.
- Press the Windows key and search for Environment Variables.
- Click Edit the system environment variables.
- Click Environment Variables at the bottom of the dialog.
- Under System variables, find and select Path, then click Edit.
- Click New and add C:\ffmpeg\bin.
- Click OK on all open dialogs to save.
Open a new terminal after editing PATH
PATH changes only take effect in new terminal sessions. Close your current VS Code terminal and open a fresh one with Ctrl+` before verifying.
- Verify the install by running in a new terminal:
ffmpeg -version
You should see version information printed to the terminal.
Still seeing 'command not found'?
Double-check that the path you added matches where you extracted the files. The bin subfolder inside your extracted directory must contain ffmpeg.exe. Make sure you opened a brand new terminal after editing PATH.
Help me add FFmpeg to my Windows PATH.
macOS
- Install FFmpeg using Homebrew by running:
brew install ffmpeg
This takes a few minutes. Once done, verify the install by running:
ffmpeg -version
You should see version information printed to the terminal.
brew: command not found?
You need Homebrew installed first. Visit https://brew.sh and follow the one-line install command, then retry.
Help me install Homebrew on macOS.
Create project files and verify Streamlit
Your project needs a few folders and files to organise templates, clips, and Python code.
- Make sure (venv) is showing in your terminal prompt. If it is not, re-run the activation command from earlier.
- Create the project folders and empty files by running these commands:
macOS
mkdir templates clips
touch highlighter.py app.py
Windows
mkdir templates, clips
New-Item highlighter.py, app.py
- Confirm the files exist by running:
ls
You should see app.py, clips, highlighter.py, templates, and venv listed in the output.
What are these folders for?
- templates/ stores the reference kill icon image that OpenCV will compare against every frame.
- clips/ is where FFmpeg will save the final highlight clips.
- highlighter.py will hold the detection and clipping logic.
- app.py will hold the Streamlit web interface.
Before you run the final check, what do you think will happen when you launch Streamlit for the first time?
- Verify that Streamlit is working by running:
streamlit hello
A new browser tab opens with the Streamlit demo app. You should see a welcome page with interactive demos you can click through.
Stop the demo when you are done
Press Ctrl+C in your terminal to stop the Streamlit demo server when you are finished looking around.
Browser did not open?
Streamlit prints a local URL in the terminal (usually http://localhost:8501). Copy that URL and paste it into your browser manually.
If the command itself fails, make sure your virtual environment is active ((venv) in your prompt) and that the pip install step completed without errors.
Help me get streamlit hello working.
Your environment is ready. Next up, you will download a League of Legends video and extract your first frames.
Download a Video and See Your First Frames
Your environment is ready with all the tools installed. Now you need actual video data to work with.
Before you can detect kills, you need to get video frames into Python. Seeing actual game frames proves your download pipeline works and shows you exactly what the detection algorithm will be scanning in later steps.
In this step, get ready to:
- Write a video download function using yt-dlp's Python API.
- Extract and save a sample frame from the downloaded video.
- Run the pipeline and see a real game frame on your screen.
Write the video download function
The yt-dlp library provides a Python API that lets you download YouTube videos programmatically. You will create a function that takes a URL and saves the video to a local folder at 720p quality.
- Open highlighter.py in VS Code (it is currently empty).
- Add the following imports and download function:
import cv2
import numpy as np
import subprocess
import os
from yt_dlp import YoutubeDL
def download_video(url, output_dir="downloads"):
# Create the downloads folder if it doesn't exist
os.makedirs(output_dir, exist_ok=True)
output_path = os.path.join(output_dir, "%(title)s.%(ext)s")
ydl_opts = {
"format": "best[height<=720]",
"outtmpl": output_path,
}
# Download the video and return the file path
with YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(url, download=True)
filename = ydl.prepare_filename(info)
return filename
What does this code do?
The imports bring in everything the project needs across all steps. cv2 is OpenCV for image processing, numpy supports array operations, subprocess runs FFmpeg commands, and YoutubeDL handles video downloads.
The download_video function creates a downloads/ folder, then uses yt-dlp to grab the best available quality at or below 720p. Limiting to 720p keeps file sizes manageable and processing fast without hurting detection accuracy.
Write frame extraction and run the script
To prove the download worked and see what your detection algorithm will be scanning, you will extract a single frame from the video and save it as an image file.
- Below the download_video function, add this __main__ block that downloads a video, seeks to 60 seconds in, and saves the frame:
if __name__ == "__main__":
url = "https://www.youtube.com/watch?v=YOUR_VIDEO_ID"
print("Downloading video...")
video_path = download_video(url)
print(f"Downloaded: {video_path}")
print("Extracting sample frame...")
# Open the downloaded video file
cap = cv2.VideoCapture(video_path)
# Seek to 60 seconds in to skip any intro or lobby
cap.set(cv2.CAP_PROP_POS_FRAMES, int(cap.get(cv2.CAP_PROP_FPS) * 60))
ret, frame = cap.read()
if ret:
cv2.imwrite("sample_frame.png", frame)
print(f"Frame saved: {frame.shape[1]}x{frame.shape[0]}")
cap.release()
What does this code do?
cv2.VideoCapture opens the video file so you can read individual frames. The cap.set call jumps to 60 seconds into the video. This skips the champion select lobby and loading screen so you land on actual gameplay.
cap.read() grabs one frame as a NumPy array. cv2.imwrite saves that array as a PNG image you can open and inspect. The print statement shows the frame dimensions (width x height) to confirm everything loaded correctly.
- Before running, replace YOUR_VIDEO_ID in the URL with the video ID of a short League of Legends highlight video on YouTube. Find any LoL highlight video (3-5 minutes is ideal) and copy its video ID from the URL.
- Save highlighter.py.
Pick a short video
Search YouTube for "League of Legends kill compilation" or "LoL highlights" and pick one that is 3-5 minutes long. Shorter videos download faster and process quicker while you are testing. You can always swap to longer videos once the pipeline is working.
Before you run the script, what do you think will happen? Will it print a frame size or an error?
- Run the script by executing this command in your terminal:
python highlighter.py
The download will take a moment depending on your internet speed. You will see yt-dlp's progress output in the terminal, followed by the frame dimensions printed (something like Frame saved: 1280x720).
- Open sample_frame.png from the file explorer in VS Code (or double-click it in lol-clipper). You should see an actual in-game screenshot showing champions, the minimap, and the HUD.
Download failed or frame not saved?
- If you see an error about the URL, double-check that you replaced YOUR_VIDEO_ID with a real video ID. The full URL should look like https://www.youtube.com/watch?v=abc123xyz.
- If the download starts but fails partway, the video may be age-restricted or region-locked. Try a different LoL highlight video.
- If sample_frame.png was not created, the video might be shorter than 60 seconds. Try a longer video or reduce the seek time in the cap.set line.
Help me debug my yt-dlp download or OpenCV frame extraction
✔️ Awesome, I've got everything!
Great. Double check you have saved highlighter.py and that sample_frame.png shows an in-game LoL screenshot.
ⓧ I'd like to double check the full code
Here is the complete highlighter.py at this point:
import cv2
import numpy as np
import subprocess
import os
from yt_dlp import YoutubeDL
def download_video(url, output_dir="downloads"):
os.makedirs(output_dir, exist_ok=True)
output_path = os.path.join(output_dir, "%(title)s.%(ext)s")
ydl_opts = {
"format": "best[height<=720]",
"outtmpl": output_path,
}
with YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(url, download=True)
filename = ydl.prepare_filename(info)
return filename
if __name__ == "__main__":
url = "https://www.youtube.com/watch?v=YOUR_VIDEO_ID"
print("Downloading video...")
video_path = download_video(url)
print(f"Downloaded: {video_path}")
print("Extracting sample frame...")
cap = cv2.VideoCapture(video_path)
cap.set(cv2.CAP_PROP_POS_FRAMES, int(cap.get(cv2.CAP_PROP_FPS) * 60))
ret, frame = cap.read()
if ret:
cv2.imwrite("sample_frame.png", frame)
print(f"Frame saved: {frame.shape[1]}x{frame.shape[0]}")
cap.release()
You have frames from a real game video in Python. Next up, you will use template matching to automatically find kill icons hiding in those frames.
Detect Kill Icons with Template Matching
You can now download a League of Legends video and pull individual frames out of it. The next challenge is finding the kills.
A typical LoL game has thousands of frames, and manually scanning through them to spot kill notifications is impossible. In this step, you will use template matching to automate this by comparing a reference kill icon against every frame.
In this step, get ready to:
- Capture a kill notification icon from your sample frame as a template image.
- Write a detection function that scans frames using OpenCV template matching.
- Run detection and observe duplicate timestamps for the same kill event.
Capture your template image
Template matching works by sliding a small reference image across a larger image, checking for similarity at each position. You need to give it the thing to look for: the kill notification icon that appears in the top-right kill feed whenever a champion dies.
- Open sample_frame.png in an image editor. Press the Windows key, type Paint, and press Enter to open Paint. On macOS, double-click the file to open it in Preview.
- In Paint, press Ctrl+O and open sample_frame.png from your lol-clipper folder.
- Look at the top-right area of the frame. This is the kill feed where kill notifications appear. Find the small sword or action icon between champion portraits.
- Click the Select tool in the Home tab (dotted rectangle icon).
- Draw a tight rectangle around just the kill icon (the small sword/action symbol between the two champion portraits). Aim for roughly 30x30 pixels. The status bar at the bottom shows the selection dimensions.
- Press Ctrl+Shift+X to crop the image down to your selection.
- Save the cropped icon as templates/kill_icon.png inside your lol-clipper folder. Use File then Save As, navigate to the templates folder, set the file name to kill_icon, and save as PNG.
What makes a good template?
The template should be tightly cropped around just the kill icon. Too much background and the match will fail when the surrounding champions change. Too small and you lose distinctive detail.
If your sample frame does not show a kill moment, pause any LoL video at a kill in the top-right feed, take a screenshot, and crop from there instead.
- Confirm the file exists by running:
ls templates/
You should see kill_icon.png listed in the output.
Don't see kill_icon.png?
Make sure you saved the file inside the templates folder (not the project root). Double-check the filename ends with .png and not .png.png (Paint sometimes double-adds extensions).
help me find where my kill_icon.png was saved
Write the detection function
Now you will write a function that loops through the video at 4 frames per second, crops each frame to the kill feed region, and runs template matching to find the icon.
- In highlighter.py, add the following function directly below the download_video function (before the if __name__ block). This first chunk sets up the video reader and prepares for scanning:
def detect_kills(video_path, template_path, fps=4, threshold=0.8):
# Load the template image in grayscale
template = cv2.imread(template_path, cv2.IMREAD_GRAYSCALE)
if template is None:
raise FileNotFoundError(f"Template not found: {template_path}")
# Open the video and calculate sampling interval
cap = cv2.VideoCapture(video_path)
video_fps = cap.get(cv2.CAP_PROP_FPS)
frame_interval = int(video_fps / fps)
detections = []
frame_count = 0
What does this code do?
The function loads your kill icon template in grayscale (template matching compares pixel intensities, so color is not needed). It then opens the video and calculates frame_interval. If the video is 30fps and you want to sample at 4fps, it checks every 7th or 8th frame instead of every single one.
- Still inside the detect_kills function, add the scanning loop immediately after frame_count = 0:
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
if frame_count % frame_interval == 0:
# Crop to the kill feed region (top-right corner)
height, width = frame.shape[:2]
roi = frame[0:int(height * 0.2), int(width * 0.7):]
roi_gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
# Run template matching and check for hits
result = cv2.matchTemplate(roi_gray, template, cv2.TM_CCOEFF_NORMED)
locations = np.where(result >= threshold)
if len(locations[0]) > 0:
timestamp = frame_count / video_fps
detections.append(timestamp)
frame_count += 1
cap.release()
return detections
What does this loop do?
For each sampled frame, it crops to just the top 20% of height and rightmost 30% of width. This isolates the kill feed region in the top-right corner where kill notifications appear.
It then converts the cropped region to grayscale and runs cv2.matchTemplate with cv2.TM_CCOEFF_NORMED. This method scores each position from -1 to 1, where higher means a better match. Any location scoring at or above the threshold (0.8) counts as a detection.
- Save highlighter.py.
Why crop to a region of interest?
Scanning the full frame would be slower and produce false positives. Kill icons only appear in the top-right corner, so cropping to that region makes detection faster and more accurate.
Run detection and observe the duplicate problem
Now you will update the __main__ block to call your new detection function and print what it finds.
- In highlighter.py, find this line inside your if __name__ == "__main__" block:
url = "https://www.youtube.com/watch?v=YOUR_VIDEO_ID"
- Add a new line directly below it that defines the template path:
template_path = os.path.join("templates", "kill_icon.png")
- Next, find the end of the __main__ block (after cap.release()). Add this code at the bottom to run detection and print results:
if os.path.exists(template_path):
print("Scanning for kills...")
detections = detect_kills(video_path, template_path)
print(f"Raw detections: {len(detections)}")
for t in detections:
print(f" {t:.2f}s")
else:
print(f"No template found at {template_path}")
print("Capture a kill icon from sample_frame.png and save it there.")
What does this code do?
It first checks that your template file exists. If it does, it calls detect_kills on the downloaded video and prints every timestamp where a match was found. If the template is missing, it reminds you to create one.
- Save highlighter.py.
Before you run this, think about what you expect the output to look like. If a kill happens at the 45 second mark and the icon stays on screen for 3 seconds, how many times do you think the scanner (checking 4 frames per second) will detect it?
- Run the detection script:
python highlighter.py
The script will re-download the video (or use the cached file), extract a sample frame, then scan every frame at 4fps looking for your template.
Look at the output timestamps. You will see the same kill appearing 3-5 times in a row with timestamps spaced 0.25 seconds apart. For example:
Scanning for kills...
Raw detections: 47
45.00s
45.25s
45.50s
45.75s
46.00s
112.25s
112.50s
112.75s
113.00s
...
Why are there duplicate detections?
The kill notification icon stays visible on screen for about 3-4 seconds. Since you are scanning at 4 frames per second, the same icon gets detected on every sampled frame while it is visible. A single kill at 45 seconds produces detections at 45.0s, 45.25s, 45.5s, 45.75s, and so on.
This is the expected behavior. Your detector works, but it needs a deduplication step to collapse these clusters into single kill events. You will fix this in the next step.
Seeing zero detections?
Your threshold might be too high for your template, or your template might include too much background. Try lowering the threshold to 0.7 in the function call, or re-crop your template tighter around just the icon.
Also make sure your sample_frame.png actually shows a kill moment. If there are no kills visible in the frame you captured, you will need a template from a different moment in the video.
help me debug why template matching is returning zero detections
✔️ Awesome, I've got everything!
Great. Make sure you have saved highlighter.py with both the download_video and detect_kills functions, plus the updated __main__ block.
ⓧ I'd like to double check the full code
Scroll up and confirm these sections are present in highlighter.py, in this order:
- Imports at the top (cv2, numpy, subprocess, os, YoutubeDL).
- The download_video function (unchanged from Step 2).
- The new detect_kills function with template loading, frame loop, ROI cropping, and template matching.
- The if __name__ == "__main__" block with template_path, sample frame extraction, and the detection conditional at the end.
Your detector is working. It finds the kill icon in every frame where it appears, which proves template matching does the job. The problem is clear: one real kill produces a cluster of 3-5 raw detections. In the next step, you will deduplicate these clusters into unique kill events and cut actual video clips around each one.
Deduplicate Detections and Generate Clips
In the previous step, you ran your detection script and saw the same kill appearing 3-5 times in the output. That happens because the kill icon stays on screen for several seconds, so every frame in that window triggers a match.
In this step, you will collapse those duplicate clusters into single kill events, then use FFmpeg to cut the source video into individual highlight clips around each unique kill.
In this step, get ready to:
- Deduplicate detection clusters into unique kill events.
- Use FFmpeg to cut video clips around each kill timestamp.
- Run the full pipeline and verify clips are generated in the clips/ folder.
Write the deduplication function
Your raw detections contain clusters of timestamps that are fractions of a second apart. A kill icon persists for about 3-4 seconds, so you get 3-5 consecutive hits for a single event.
The fix is simple: walk through the sorted timestamps and only keep a detection if it is at least 10 seconds after the previous one.
- In highlighter.py, add this function below detect_kills and above the if __name__ == "__main__" block:
def deduplicate_detections(detections, min_gap=10):
if not detections:
return []
unique = [detections[0]]
for t in detections[1:]:
if t - unique[-1] >= min_gap:
unique.append(t)
return unique
What does this code do?
- min_gap=10 sets the minimum time between two separate kill events to 10 seconds.
- The function starts by keeping the first detection, then only adds a new one if it is at least 10 seconds after the last kept timestamp.
- This collapses a cluster like [45.0, 45.25, 45.5, 45.75] into just [45.0].
- Save highlighter.py.
- Verify the function works by running this quick test in your terminal:
python -c "from highlighter import deduplicate_detections; print(deduplicate_detections([45.0, 45.25, 45.5, 120.0, 120.25]))"
You should see [45.0, 120.0] printed. Two clusters collapsed into two unique timestamps.
Seeing an ImportError?
- Make sure your virtual environment is activated. You should see (venv) at the start of your terminal prompt.
- Confirm you saved highlighter.py after adding the function.
- Check that deduplicate_detections is defined at the module level (not indented inside another function).
Can't figure out what's wrong? Help me debug why my deduplicate_detections function isn't importing correctly from highlighter.py
Write the clipping function using FFmpeg
Now that you can identify unique kill timestamps, you need to cut the original video around each one. FFmpeg handles this by seeking to a start time and extracting a fixed duration.
- In highlighter.py, add this function below deduplicate_detections and above the if __name__ == "__main__" block:
def clip_highlights(video_path, timestamps, output_dir="clips", before=8, after=3):
os.makedirs(output_dir, exist_ok=True)
clip_paths = []
for i, t in enumerate(timestamps):
start = max(0, t - before)
duration = before + after
output_path = os.path.join(output_dir, f"kill_{i+1}.mp4")
cmd = [
"ffmpeg", "-y",
"-ss", str(start),
"-i", video_path,
"-t", str(duration),
"-c:v", "libx264",
"-c:a", "aac",
output_path,
]
subprocess.run(cmd, capture_output=True)
clip_paths.append(output_path)
return clip_paths
What does this code do?
- For each kill timestamp, the function calculates a start point 8 seconds before the kill and cuts an 11-second clip (8 before + 3 after).
- -ss seeks to the start time and -t sets the clip duration.
- -c:v libx264 -c:a aac re-encodes the clip with standard H.264 video and AAC audio codecs for broad compatibility.
- -y automatically overwrites existing files so re-runs don't stall waiting for confirmation.
- Each clip is saved as clips/kill_1.mp4, clips/kill_2.mp4, and so on.
- Save highlighter.py.
Run the full pipeline end-to-end
Both functions are ready. Now you need to update the __main__ block to call them in sequence so one script run does everything: download, detect, deduplicate, and clip.
- In highlighter.py, scroll down to the if __name__ == "__main__" block.
- Find these lines inside the if os.path.exists(template_path): block:
print(f"Raw detections: {len(detections)}")
for t in detections:
print(f" {t:.2f}s")
- Replace the for t in detections: loop and its print line with the following, so the section now looks like this:
print(f"Raw detections: {len(detections)}")
unique_kills = deduplicate_detections(detections)
print(f"Unique kills: {len(unique_kills)}")
print("Generating clips...")
clips = clip_highlights(video_path, unique_kills)
print(f"Generated {len(clips)} clips in clips/ folder")
What changed?
Instead of printing every raw timestamp, the script now feeds the detections through deduplicate_detections to collapse clusters, then passes the unique timestamps to clip_highlights to generate MP4 files.
- Save highlighter.py.
Before you run this, think about what you expect to happen. The script will re-download (or use the cached video), scan every frame, and produce clips. Do you think the number of unique kills will be much smaller than the raw detections?
- Run the full pipeline:
python highlighter.py
The script prints progress for each stage. At the end, you should see output like:
Raw detections: 47
Unique kills: 12
Generated 12 clips in clips/ folder
Your numbers will vary depending on the video, but the unique kills count should be much smaller than the raw detections. That proves deduplication is working.
- Open the clips/ folder and play one of the generated MP4 files to confirm it contains an actual kill moment.
No clips generated or FFmpeg errors?
- Run ffmpeg -version in your terminal to confirm FFmpeg is on your PATH. If you get "command not found", revisit the FFmpeg install from Step 1.
- If you see 0 raw detections, your template might not match the video's kill feed icons. Try lowering the threshold to 0.7 in the detect_kills call.
- If clips are generated but won't play, check that the video file in downloads/ isn't corrupted by playing it directly.
Still stuck? Help me debug why my LoL Kill Clipper pipeline isn't generating clips after running python highlighter.py
✔️ Awesome, I've got everything!
Great. Confirm you have saved highlighter.py and that your clips/ folder contains playable MP4 files.
ⓧ I'd like to double check the full code
Your highlighter.py should now contain these sections in order. Scroll up through the file and confirm each one is present:
- Imports (cv2, numpy, subprocess, os, YoutubeDL).
- download_video function.
- detect_kills function.
- deduplicate_detections function (new this step).
- clip_highlights function (new this step).
- if __name__ == "__main__" block that calls download, detect, deduplicate, and clip in sequence.
Your pipeline now takes a YouTube URL and produces individual highlight clips, fully automated. Next up, you will wrap this entire workflow in a Streamlit web UI so anyone can paste a URL and get clips without touching the terminal.
Build the Streamlit UI
Your detection pipeline works end-to-end from the terminal. You can download a video, find kills, deduplicate them, and cut clips. But running a Python script and editing URLs in source code every time is clunky.
In this step, you will wrap the entire pipeline in a Streamlit web app. The result is a local page where you paste a URL, click a button, and get back playable video clips with download links.
In this step, get ready to:
- Build the input interface with a URL field, threshold slider, and template warning.
- Wire up the processing pipeline with real-time status messages.
- Display results with inline video players and download buttons.
Build the input interface in app.py
The first thing a user sees is the input form. You need a title, a text field for the YouTube URL, and a slider for tuning detection sensitivity.
- Open app.py in VS Code and add the following code:
import streamlit as st
import os
from highlighter import download_video, detect_kills, deduplicate_detections, clip_highlights
# App title and description
st.title("LoL Kill Clipper")
st.write("Paste a League of Legends YouTube video URL to automatically detect and clip kills.")
# Input widgets
url = st.text_input("YouTube URL")
template_path = os.path.join("templates", "kill_icon.png")
# Warn if the template image is missing
if not os.path.exists(template_path):
st.warning("No template image found. Place a kill icon screenshot at templates/kill_icon.png")
# Threshold slider for tuning detection sensitivity
threshold = st.slider("Detection Threshold", 0.5, 1.0, 0.8, 0.05)
What does this code do?
The import on line 3 pulls in all four functions you wrote in highlighter.py. This means app.py can call your pipeline without duplicating any logic.
The st.slider creates a draggable control ranging from 0.5 to 1.0 with a default of 0.8. Lower values catch more matches (including false positives). Higher values are stricter.
- Save app.py.
- Run the app by executing this command in your terminal:
streamlit run app.py
A browser tab opens showing the title, a text input, a warning about the template (if applicable), and a threshold slider.
Browser didn't open?
Streamlit prints a local URL in your terminal (usually http://localhost:8501). Copy and paste it into your browser manually.
If you see a ModuleNotFoundError, make sure your virtual environment is activated.
Help me debug my Streamlit app not opening.
Wire up the processing pipeline
The UI looks good, but clicking a button should trigger the full pipeline. Streamlit's st.status component creates an expandable container that shows progress messages as each stage runs.
- Stop the running Streamlit server by pressing Ctrl+C in your terminal.
- In app.py, add the following code below the threshold = st.slider(...) line:
if st.button("Find Kills") and url:
if not os.path.exists(template_path):
st.error("Template image not found. Add templates/kill_icon.png first.")
else:
with st.status("Processing...", expanded=True) as status:
st.write("Downloading video...")
video_path = download_video(url)
st.write("Scanning for kills...")
detections = detect_kills(video_path, template_path, threshold=threshold)
st.write(f"Found {len(detections)} raw detections")
st.write("Deduplicating...")
unique_kills = deduplicate_detections(detections)
st.write(f"Identified {len(unique_kills)} unique kills")
st.write("Generating clips...")
clips = clip_highlights(video_path, unique_kills)
status.update(label="Done!", state="complete")
st.success(f"Generated {len(clips)} highlight clips!")
What does this code do?
The if st.button("Find Kills") and url: line only runs the pipeline when the button is clicked AND a URL has been entered.
Inside st.status, each st.write call adds a live progress line. The user sees each stage appear in real time. When all stages finish, status.update(label="Done!", state="complete") swaps the spinner for a checkmark.
- Save app.py and restart the app:
streamlit run app.py
- Paste a LoL YouTube URL into the text field and click Find Kills.
You should see the status container expand with progress messages appearing one by one, ending with a green success banner showing how many clips were generated.
Seeing a NameError or ImportError?
Make sure the import line at the top of app.py says from highlighter import download_video, detect_kills, deduplicate_detections, clip_highlights. All four function names must match exactly.
Help me fix my Streamlit import error.
Display results with video players and download buttons
The pipeline runs and prints a count, but the user cannot watch or save the clips yet. You need a loop that renders each clip inline with a download button.
- In app.py, find the st.success(f"Generated {len(clips)} highlight clips!") line. Add the following code directly below it, at the same indentation level (inside the else block):
for i, clip_path in enumerate(clips):
st.subheader(f"Kill {i+1}")
st.video(clip_path)
with open(clip_path, "rb") as f:
st.download_button(
f"Download Kill {i+1}",
f,
file_name=f"kill_{i+1}.mp4",
mime="video/mp4",
)
What does this code do?
The for loop iterates over each clip path returned by clip_highlights. For each clip, st.video embeds a playable video player directly in the page.
The st.download_button opens the file in binary mode and serves it as a downloadable MP4. The user can save any clip to their machine with one click.
- Save app.py.
Before you test the full flow, what do you expect to see below the success message after the pipeline finishes?
- Restart the app by running:
streamlit run app.py
- Paste a LoL YouTube URL, click Find Kills, and wait for processing to complete.
After the green success banner, you should see a heading for each kill, a playable video embedded in the page, and a download button beneath each one.
Videos not appearing?
Check that the for loop is indented inside the else block (8 spaces deep), at the same level as st.success. If it is outside the if st.button(...) block, Streamlit will try to display clips before they exist.
Help me fix my clip display loop in Streamlit.
✔️ Awesome, I've got everything!
Great. Double check you have saved app.py and the app runs without errors.
ⓧ I'd like to double check the full code
Your app.py should contain three sections in this order:
- Imports, title, description, URL input, template path, warning, and threshold slider (the first chunk you added).
- The if st.button("Find Kills") and url: block containing the template error check, the st.status context manager calling all four pipeline functions, and the st.success line.
- The for i, clip_path in enumerate(clips): loop with st.video and st.download_button for each clip, indented inside the else block.
Scroll up through this step and confirm each chunk is present and in order.
Your LoL Kill Clipper is fully functional. You have a local web app that takes any League of Legends YouTube URL and outputs playable, downloadable highlight clips. Next up, the Secret Mission challenges you to add a detection preview mode so you can visually verify what the algorithm matched.
Secret mission
Add Detection Preview Mode
Your clipper works, but how do you know the threshold is right? Too low catches false positives. Too high misses real kills. In this extension, you will add a detection preview that shows the actual matched frames with bounding boxes and confidence scores, so you can visually verify and tune your detection.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Downloaded video files in the downloads/ folder (can be over 100MB per video).
- Generated clip files in the clips/ folder.
- sample_frame.png saved in the project root.
- The lol-clipper project folder on your Desktop (includes virtual environment, code, and templates).
- FFmpeg installed at C:\ffmpeg with a PATH entry.
Keep everything running
No action needed. Choose this if you plan to keep using the clipper on future videos.
- Your project files, virtual environment, and templates stay ready to use.
- The template image in templates/ is tiny and worth keeping for reuse.
- FFmpeg remains available on your system for future video processing.
Pause - I'll come back to this later
Free up disk space by removing large media files while keeping your code intact.
- Open a terminal in your lol-clipper folder.
- Delete the video and clip folders by running the command for your operating system.
macOS
rm -rf downloads clips sample_frame.png
Windows
rmdir /s /q downloads clips
del sample_frame.png
Your code, virtual environment, and template image remain in place for next time.
Delete - I don't want to use this again
Remove all project files and start fresh.
- Delete the entire project folder by running:
macOS
cd ~/Desktop
rm -rf lol-clipper
Windows
cd %USERPROFILE%\Desktop
rmdir /s /q lol-clipper
- To remove FFmpeg, delete the C:\ffmpeg folder.
- Remove C:\ffmpeg\bin from your system PATH. Open Settings, search for environment variables, select Path in the system variables, click Edit, and delete the C:\ffmpeg\bin entry.
Nice Work!
Nice Work!
You just built a local video processing tool that turns any League of Legends YouTube video into individual kill highlight clips. From downloading video to detecting patterns with computer vision to cutting clips automatically, you own the entire pipeline.
You've learned how to:
- Built a Python video processing pipeline that downloads YouTube videos with yt-dlp and uses OpenCV template matching to detect kill icons in video frames.
- Solved the duplicate-detection problem with time-windowed deduplication and used FFmpeg to programmatically cut video clips at precise timestamps.
- Wrapped everything in a Streamlit web UI with progress feedback, inline video playback, and download buttons.
- Secret Mission: Added a detection preview mode with bounding boxes and confidence scores for visual threshold tuning.
Ready to quiz yourself?