- Posted on
- • Artificial Intelligence
Artificial Intelligence Media Server Management
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
Artificial Intelligence Media Server Management: A Bash‑First Guide for Linux
If your media library grows faster than you can tag, subtitle, or de‑duplicate, you’re not alone. Hours lost to manual sorting, stale metadata, missing subtitles, and duplicates make even the best servers feel chaotic. The good news: modern, local AI tooling can do the heavy lifting. In this guide, you’ll build a Linux- and Bash-centric workflow that uses open-source AI models to organize, tag, subtitle, and monitor your media server—without shipping data to third parties.
What you’ll get:
A practical, local-first AI pipeline for media servers (Plex/Jellyfin/Emby, or plain folders)
Bash scripts that index media, auto-tag content, generate subtitles, and spot duplicates
Install steps for apt, dnf, and zypper
Real-world patterns you can adopt today
Why this is worth your weekend
Discoverability: Tags, transcripts, and thumbnails make searching and browsing your library actually fun.
Accessibility and compliance: Auto-generated subtitles help everyone, including the hearing impaired or non-native speakers.
Storage sanity: Perceptual hashing catches near-duplicates that byte-level checksums miss.
Local, private, and efficient: Run everything offline on your own hardware; scale up later with GPU if you want.
Mature building blocks: ffmpeg, Whisper (speech-to-text), CLIP (image understanding), and robust Linux tooling are stable and well-documented.
Prerequisites (Linux packages)
Install core tools for media inspection, hashing, monitoring, and Python builds.
Debian/Ubuntu (apt):
sudo apt update
sudo apt install -y \
ffmpeg mediainfo exiftool jq inotify-tools sqlite3 rhash \
python3 python3-venv python3-dev git gcc pkg-config libffi-dev
Fedora/RHEL/CentOS (dnf):
# Fedora may require RPM Fusion for ffmpeg:
sudo dnf install -y 'dnf-command(config-manager)'
sudo dnf config-manager --set-enabled crb >/dev/null 2>&1 || true
sudo dnf install -y https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm
sudo dnf install -y \
ffmpeg mediainfo perl-Image-ExifTool jq inotify-tools sqlite rhash \
python3 python3-venv python3-devel git gcc gcc-c++ make pkgconf-pkg-config libffi-devel
openSUSE (zypper):
sudo zypper refresh
sudo zypper install -y \
ffmpeg mediainfo perl-Image-ExifTool jq inotify-tools sqlite3 rhash \
python3 python3-venv python3-devel git gcc pkg-config libffi-devel
Notes:
Some Fedora/RHEL variants need EPEL/RPM Fusion for ffmpeg.
On Debian/Ubuntu,
exiftoolmay arrive via thelibimage-exiftool-perlpackage (theexiftoolalias also works on most systems).
Python environment and AI libs
Create an isolated environment and install CPU-friendly packages (works anywhere; GPU is optional later).
python3 -m venv ~/ai-media
source ~/ai-media/bin/activate
pip install --upgrade pip
# PyTorch CPU wheels
pip install --index-url https://download.pytorch.org/whl/cpu torch torchvision torchaudio
# AI helpers
pip install openai-whisper clip-anytorch pillow numpy imagehash tqdm
Optional (GPU): Install the CUDA-enabled PyTorch per the official PyTorch instructions, then re-install Whisper/CLIP as above. This dramatically speeds up transcription and tagging.
Database bootstrap
We’ll keep everything simple and local with SQLite.
sqlite3 ~/media.db <<'SQL'
CREATE TABLE IF NOT EXISTS media (
id INTEGER PRIMARY KEY,
path TEXT UNIQUE,
sha256 TEXT,
duration REAL,
width INTEGER,
height INTEGER,
fps REAL,
codec TEXT,
phash TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS tags (
media_id INTEGER,
label TEXT,
score REAL,
FOREIGN KEY(media_id) REFERENCES media(id)
);
SQL
Actionable blueprint (5 steps with scripts)
1) Index your library: inventory, metadata, and checksums
Use ffmpeg/mediainfo to extract technical metadata and rhash for integrity. This creates a source of truth for everything else.
Save as index_media.sh:
#!/usr/bin/env bash
set -euo pipefail
DB="${DB:-$HOME/media.db}"
ROOT="${1:-/srv/media}"
export LC_ALL=C
find "$ROOT" -type f \( -iname '*.mp4' -o -iname '*.mkv' -o -iname '*.mov' -o -iname '*.avi' \) -print0 |
while IFS= read -r -d '' f; do
echo "Indexing: $f"
sha=$(rhash --sha256 "$f" | awk '{print $1}')
meta=$(mediainfo --Output=JSON "$f")
dur=$(jq -r '.media.track[]|select(.["@type"]=="General")|.Duration|tonumber/1000' <<<"$meta")
w=$(jq -r '.media.track[]|select(.["@type"]=="Video")|.Width|tonumber' <<<"$meta")
h=$(jq -r '.media.track[]|select(.["@type"]=="Video")|.Height|tonumber' <<<"$meta")
fps=$(jq -r '.media.track[]|select(.["@type"]=="Video")|.FrameRate|tonumber' <<<"$meta")
codec=$(jq -r '.media.track[]|select(.["@type"]=="Video")|.Format' <<<"$meta")
sqlite3 "$DB" <<SQL
INSERT INTO media(path, sha256, duration, width, height, fps, codec)
VALUES('$(printf "%s" "$f" | sed "s/'/''/g")', '$sha', $dur, $w, $h, $fps, '$(printf "%s" "$codec" | sed "s/'/''/g")')
ON CONFLICT(path) DO UPDATE SET sha256=excluded.sha256, duration=excluded.duration,
width=excluded.width, height=excluded.height, fps=excluded.fps, codec=excluded.codec;
SQL
done
Run it:
bash index_media.sh /srv/media
Real-world example: Use this database to generate dashboards of codec/bitrate distributions before a re-encode campaign, or to confirm integrity after a ZFS scrub.
2) AI tagging with CLIP: auto-label content from a representative frame
We’ll pick an interesting frame using scene detection, then score it against a human-readable label list.
Save as tag_with_clip.py:
#!/usr/bin/env python3
import sys, json, torch
from PIL import Image
import clip # provided by clip-anytorch
labels = [
"sports", "news", "cooking", "nature", "gaming",
"music performance", "lecture", "travel", "animation", "documentary"
]
device = "cpu"
model, preprocess = clip.load("ViT-B/32", device=device)
text_tokens = clip.tokenize(labels).to(device)
img_path = sys.argv[1]
image = preprocess(Image.open(img_path)).unsqueeze(0).to(device)
with torch.no_grad():
logits_per_image, _ = model(image, text_tokens)
probs = logits_per_image.softmax(dim=-1).cpu().numpy()[0]
top = sorted(zip(labels, probs), key=lambda x: x[1], reverse=True)[:5]
print(json.dumps({"tags":[{"label": l, "score": float(s)} for l, s in top]}))
Save as autotag.sh:
#!/usr/bin/env bash
set -euo pipefail
DB="${DB:-$HOME/media.db}"
ROOT="${1:-/srv/media}"
TMP="$(mktemp -d)"
cleanup(){ rm -rf "$TMP"; }
trap cleanup EXIT
while IFS= read -r path; do
echo "Tagging: $path"
# Try to capture a scene-change frame; fall back to 10s mark
out="$TMP/frame.jpg"
ffmpeg -nostdin -v error -y -i "$path" -vf "select='gt(scene,0.4)',scale=-2:360" -frames:v 1 "$out" \
|| ffmpeg -nostdin -v error -y -ss 00:00:10 -i "$path" -frames:v 1 -q:v 2 "$out"
# Compute tags
res=$(python3 tag_with_clip.py "$out")
# Insert into DB
id=$(sqlite3 "$DB" "SELECT id FROM media WHERE path = '$(printf "%s" "$path" | sed "s/'/''/g")';")
if [ -n "$id" ]; then
echo "$res" | jq -c '.tags[]' | while read -r t; do
label=$(jq -r '.label' <<<"$t")
score=$(jq -r '.score' <<<"$t")
sqlite3 "$DB" "INSERT INTO tags(media_id,label,score) VALUES($id,'$(printf "%s" "$label" | sed "s/'/''/g")',$score);"
done
fi
done < <(sqlite3 -csv "$DB" "SELECT path FROM media;" | tr -d '\r')
Run it:
source ~/ai-media/bin/activate
bash autotag.sh /srv/media
Real-world example: Use tags like “lecture”, “music performance”, or “gaming” to auto-create smart collections in Jellyfin/Plex using your DB as the upstream metadata source.
3) Speech‑to‑text subtitles with Whisper
Generate sidecar subtitles (VTT) locally. This immediately boosts accessibility and searchability.
Transcribe a file:
source ~/ai-media/bin/activate
whisper "/srv/media/MyVideo.mp4" --model small --language en --task transcribe --output_format vtt --output_dir "/srv/media"
Batch it:
find /srv/media -type f -iname '*.mp4' -o -iname '*.mkv' | while read -r f; do
whisper "$f" --model small --language en --task transcribe --output_format vtt --output_dir "$(dirname "$f")"
done
Attach as sidecar .vtt, or remux into MKV if you prefer embedded subs:
# Embed VTT into MKV without re-encoding
ffmpeg -nostdin -v error -i "movie.mkv" -i "movie.vtt" -map 0 -map 1 -c copy -metadata:s:s:0 language=eng "movie.with-subs.mkv"
Tip: If you have a GPU, switch to a larger Whisper model for significantly better accuracy and speed.
4) Near-duplicate detection with perceptual hashing (pHash)
Byte-level hashes won’t catch re-encodes or slight crops. Perceptual hashing compares visual similarity.
Save as compute_phash.py:
#!/usr/bin/env python3
import sys, sqlite3, tempfile, subprocess
from PIL import Image
import imagehash
db = sys.argv[1]
conn = sqlite3.connect(db)
c = conn.cursor()
c.execute("SELECT id, path FROM media WHERE phash IS NULL")
rows = c.fetchall()
for mid, path in rows:
with tempfile.NamedTemporaryFile(suffix=".jpg") as tmp:
# Sample a mid-frame; adjust seek as needed
subprocess.run([
"ffmpeg","-nostdin","-v","error","-y","-ss","00:00:10","-i",path,
"-frames:v","1","-q:v","2",tmp.name
], check=True)
img = Image.open(tmp.name).convert("RGB")
ph = imagehash.phash(img) # 64-bit hash
c.execute("UPDATE media SET phash=? WHERE id=?", (str(ph), mid))
conn.commit()
Run it:
source ~/ai-media/bin/activate
python3 compute_phash.py ~/media.db
Find likely duplicates (Hamming distance <= 8 is a common heuristic). Save as find_dupes.py:
#!/usr/bin/env python3
import sys, sqlite3, itertools, imagehash
db = sys.argv[1]
conn = sqlite3.connect(db)
rows = conn.execute("SELECT id, path, phash FROM media WHERE phash IS NOT NULL").fetchall()
def to_hash(h): return imagehash.hex_to_hash(h)
pairs = []
for (id1,p1,h1),(id2,p2,h2) in itertools.combinations(rows, 2):
d = to_hash(h1) - to_hash(h2) # Hamming distance
if d <= 8:
pairs.append((d, p1, p2))
pairs.sort()
for d, a, b in pairs[:50]:
print(f"{d}\t{a}\t{b}")
Real-world example: Use this to flag archival candidates before a storage migration—keep the highest-resolution copy when near-duplicates exist.
5) Automate on arrival: inotify-driven pipeline
Kick off indexing, tagging, and transcription when new files land.
Save as watch_and_process.sh:
#!/usr/bin/env bash
set -euo pipefail
ROOT="${1:-/srv/media}"
DB="${DB:-$HOME/media.db}"
VENV="${VENV:-$HOME/ai-media}"
source "$VENV/bin/activate"
inotifywait -m -r -e close_write,move,create --format '%w%f' "$ROOT" | \
while read -r f; do
case "${f,,}" in
*.mp4|*.mkv|*.mov|*.avi)
echo "New media: $f"
bash index_media.sh "$ROOT"
# Optional: quick tags + subtitles
whisper "$f" --model small --language en --task transcribe --output_format vtt --output_dir "$(dirname "$f")" || true
;;
esac
done
Run it in a tmux/screen session, or wire it into a systemd user service for reliability.
Performance, privacy, and scaling tips
Start small: Run Whisper “small” or “base” on CPU, then iterate. Add a GPU later if queue times grow.
Local-first: All processing here is offline by default. No API keys, no uploads.
Hardware acceleration: For transcodes/thumbnails, use VA-API/NVENC with ffmpeg if available.
Model customization: Adjust CLIP labels to match your library (e.g., “kids”, “4K UHD”, “time-lapse”, “coding tutorial”).
Integrations: Export tags to NFO, Jellyfin/Plex collections, or write custom agents that read from SQLite.
Troubleshooting quick hits
ffmpeg not found on Fedora/RHEL: enable RPM Fusion (see install section).
Whisper too slow on CPU: try
--model tinyor--model base, or enable GPU PyTorch builds.CLIP import error: ensure you installed
clip-anytorchand that your venv is active.Python wheels failing to build: make sure
python3-devel(dnf/zypper) orpython3-dev(apt) andlibffidev packages are installed.
Conclusion and next steps (CTA)
You now have a private, automatable AI assistant for your media server:
Index and verify files with a single command
Auto-tag content for smarter browsing
Generate subtitles for accessibility and search
Catch near-duplicates before they waste space
Trigger everything on new arrivals
Next steps:
Tune CLIP labels to your library and push tags into your media server’s metadata.
Schedule nightly runs with cron or systemd timers.
Add GPU acceleration when you’re ready to scale.
If you found this useful, pick one step—subtitles or tagging—and ship it today. Your future self (and your viewers) will thank you.