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, exiftool may arrive via the libimage-exiftool-perl package (the exiftool alias 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 tiny or --model base, or enable GPU PyTorch builds.

  • CLIP import error: ensure you installed clip-anytorch and that your venv is active.

  • Python wheels failing to build: make sure python3-devel (dnf/zypper) or python3-dev (apt) and libffi dev 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.