media-transcriber/src/indexer.py
Jarian 0da74d39a0 init: media-transcriber pipeline
- whisper.cpp base.en transcription on RTX 5060 Ti (GPU 1)
- 25,439 videos indexed from /mnt/mediaserver/
- pipeline: ffmpeg extract -> whisper-server infer -> save SRT
- thermal cycle: 45min work / 15min rest
- fixes: curl quotes for spaces, --convert + WorkingDirectory=/tmp
2026-07-07 04:47:18 +00:00

96 lines
3.2 KiB
Python

import os
import json
import hashlib
import subprocess
from datetime import datetime, timezone
VIDEO_EXTENSIONS = {".mp4", ".mkv", ".avi", ".mov", ".webm", ".flv", ".wmv", ".m4v"}
BACKUP_KEYWORDS = {"backup", "backups", "#recycle"}
MEDIA_ROOT = os.environ.get("MEDIA_ROOT", "/mnt/mediaserver")
SCAN_DIRS = os.environ.get("SCAN_DIRS", "Youtube,Movies,TV").split(",")
INDEX_PATH = os.environ.get("INDEX_PATH", "/home/jarian/projects/media-transcriber/index.json")
TRANSCRIPTS_DIR = os.environ.get("TRANSCRIPTS_DIR", "/home/jarian/projects/media-transcriber/transcripts")
def is_video(path):
return os.path.splitext(path)[1].lower() in VIDEO_EXTENSIONS
def is_backup(path):
parts = path.replace(MEDIA_ROOT, "").split("/")
return any(k.lower() in {p.lower() for p in parts} for k in BACKUP_KEYWORDS)
def video_hash(filepath):
stat = os.stat(filepath)
raw = f"{stat.st_size}:{stat.st_mtime_ns}:{filepath}"
return hashlib.sha256(raw.encode()).hexdigest()[:16]
def get_duration(filepath):
try:
result = subprocess.run(
["ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1", filepath],
capture_output=True, text=True, timeout=10
)
return round(float(result.stdout.strip()), 2)
except Exception:
return None
def transcript_exists(rel_path):
full_path = os.path.join(MEDIA_ROOT, rel_path)
parent = os.path.dirname(full_path)
trans_dir = os.path.join(parent, "_transcriptions")
base = os.path.splitext(os.path.basename(full_path))[0]
for ext in (".srt", ".vtt", ".txt", ".json"):
candidate = os.path.join(trans_dir, f"{base}_transcription{ext}")
if os.path.exists(candidate):
return True, candidate
return False, None
def scan(skip_duration=False):
videos = {}
now = datetime.now(timezone.utc).isoformat()
for scan_dir in SCAN_DIRS:
root = os.path.join(MEDIA_ROOT, scan_dir)
if not os.path.isdir(root):
continue
for dirpath, _, filenames in os.walk(root):
for fname in filenames:
full = os.path.join(dirpath, fname)
if not is_video(full) or is_backup(full):
continue
rel = os.path.relpath(full, MEDIA_ROOT)
has_transcript, transcript_file = transcript_exists(rel)
vid_hash = video_hash(full)
stat = os.stat(full)
videos[rel] = {
"path": full,
"hash": vid_hash,
"duration_seconds": None if skip_duration else get_duration(full),
"size_bytes": stat.st_size,
"transcribed": has_transcript,
"transcript_file": transcript_file,
"last_scanned": now,
}
transcribed_count = sum(1 for v in videos.values() if v["transcribed"])
index = {
"version": "1",
"last_updated": now,
"total_videos": len(videos),
"transcribed": transcribed_count,
"videos": videos,
}
tmp = INDEX_PATH + ".tmp"
with open(tmp, "w") as f:
json.dump(index, f, indent=2)
os.replace(tmp, INDEX_PATH)
return index