- 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
96 lines
3.2 KiB
Python
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
|