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
This commit is contained in:
commit
0da74d39a0
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Virtual env
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# Index (25k videos, large)
|
||||||
|
index.json
|
||||||
|
index.json.tmp
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Transcripts
|
||||||
|
transcripts/
|
||||||
|
|
||||||
|
# Temp files
|
||||||
|
*.wav
|
||||||
|
tmp/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
39
mt.py
Executable file
39
mt.py
Executable file
@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: mt.py <scan|transcribe|continuous|status>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
cmd = sys.argv[1]
|
||||||
|
if cmd == "scan":
|
||||||
|
import json
|
||||||
|
from indexer import scan
|
||||||
|
skip_dur = "--duration" not in sys.argv
|
||||||
|
idx = scan(skip_duration=skip_dur)
|
||||||
|
print(json.dumps({
|
||||||
|
"total": idx["total_videos"],
|
||||||
|
"transcribed": idx["transcribed"],
|
||||||
|
"pending": idx["total_videos"] - idx["transcribed"],
|
||||||
|
}, indent=2))
|
||||||
|
elif cmd in ("transcribe", "continuous"):
|
||||||
|
from worker import run_once, run_continuous
|
||||||
|
run_once() if cmd == "transcribe" else run_continuous()
|
||||||
|
elif cmd == "status":
|
||||||
|
import json
|
||||||
|
with open("/home/jarian/projects/media-transcriber/index.json") as f:
|
||||||
|
idx = json.load(f)
|
||||||
|
print(f"Total: {idx['total_videos']}")
|
||||||
|
print(f"Done: {idx['transcribed']}")
|
||||||
|
print(f"Pending: {idx['total_videos'] - idx['transcribed']}")
|
||||||
|
print(f"Updated: {idx['last_updated']}")
|
||||||
|
else:
|
||||||
|
print(f"Unknown command: {cmd}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
61
setup.sh
Executable file
61
setup.sh
Executable file
@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PROJECT_DIR="/home/jarian/projects/media-transcriber"
|
||||||
|
WHISPER_CPP="/home/jarian/whisper.cpp"
|
||||||
|
|
||||||
|
echo "=== Media Transcriber Setup ==="
|
||||||
|
|
||||||
|
# Create venv
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Install deps
|
||||||
|
pip install --upgrade pip
|
||||||
|
|
||||||
|
# Ensure ffmpeg available
|
||||||
|
if ! command -v ffmpeg &>/dev/null; then
|
||||||
|
echo "ERROR: ffmpeg not found. Install with: sudo apt install ffmpeg"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! command -v ffprobe &>/dev/null; then
|
||||||
|
echo "ERROR: ffprobe not found. Install with: sudo apt install ffmpeg"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check whisper-server binary
|
||||||
|
if [ ! -f "$WHISPER_CPP/build/bin/whisper-server" ]; then
|
||||||
|
echo "ERROR: whisper-server not built. Run:"
|
||||||
|
echo " cd $WHISPER_CPP && cmake -B build -DWHISPER_CUDA=ON && cmake --build build"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Download small model if missing
|
||||||
|
MODEL_PATH="$WHISPER_CPP/models/ggml-small.bin"
|
||||||
|
if [ ! -f "$MODEL_PATH" ]; then
|
||||||
|
echo "Downloading whisper small model..."
|
||||||
|
bash "$WHISPER_CPP/models/download-ggml-model.sh" small
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Initial scan
|
||||||
|
echo "Running initial video scan..."
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
source venv/bin/activate
|
||||||
|
python src/worker.py scan
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Setup complete ==="
|
||||||
|
echo ""
|
||||||
|
echo "To start whisper-server on GPU 1:"
|
||||||
|
echo " CUDA_VISIBLE_DEVICES=1 $WHISPER_CPP/build/bin/whisper-server \\"
|
||||||
|
echo " -m $MODEL_PATH -ngl 99 -ps 16384"
|
||||||
|
echo ""
|
||||||
|
echo "Or enable systemd service:"
|
||||||
|
echo " sudo cp systemd/whisper-server.service /etc/systemd/system/"
|
||||||
|
echo " sudo systemctl enable --start whisper-server"
|
||||||
|
echo ""
|
||||||
|
echo "To enable nightly transcription:"
|
||||||
|
echo " sudo cp systemd/media-transcriber.timer /etc/systemd/system/"
|
||||||
|
echo " sudo cp systemd/media-transcriber.service /etc/systemd/system/"
|
||||||
|
echo " sudo systemctl enable --now media-transcriber.timer"
|
||||||
95
src/indexer.py
Normal file
95
src/indexer.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
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
|
||||||
302
src/worker.py
Normal file
302
src/worker.py
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import subprocess
|
||||||
|
from queue import Queue, Empty
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(__file__))
|
||||||
|
from indexer import INDEX_PATH, scan, video_hash
|
||||||
|
|
||||||
|
WHISPER_SERVER = os.environ.get("WHISPER_SERVER_URL", "http://127.0.0.1:8888")
|
||||||
|
WHISPER_MODEL = os.environ.get("WHISPER_MODEL", "base.en")
|
||||||
|
WORK_SECONDS = int(os.environ.get("WORK_SECONDS", "2700")) # 45 min active
|
||||||
|
REST_SECONDS = int(os.environ.get("REST_SECONDS", "900")) # 15 min thermal break
|
||||||
|
BATCH_LOG = os.environ.get("BATCH_LOG", "/home/jarian/projects/media-transcriber/logs/batch.log")
|
||||||
|
SPEED_FACTOR = 2.5 # base.en ~2-3x realtime, safety margin
|
||||||
|
|
||||||
|
stop_event = threading.Event()
|
||||||
|
|
||||||
|
|
||||||
|
def load_index():
|
||||||
|
with open(INDEX_PATH) as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def save_index(index):
|
||||||
|
tmp = INDEX_PATH + ".tmp"
|
||||||
|
with open(tmp, "w") as f:
|
||||||
|
json.dump(index, f, indent=2)
|
||||||
|
os.replace(tmp, INDEX_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg):
|
||||||
|
ts = datetime.now(timezone.utc).isoformat()
|
||||||
|
line = f"[{ts}] {msg}"
|
||||||
|
print(line, flush=True)
|
||||||
|
os.makedirs(os.path.dirname(BATCH_LOG), exist_ok=True)
|
||||||
|
with open(BATCH_LOG, "a") as f:
|
||||||
|
f.write(line + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Pipeline stages ---
|
||||||
|
|
||||||
|
class ExtractWorker:
|
||||||
|
def __init__(self, video_queue, audio_queue):
|
||||||
|
self.video_queue = video_queue
|
||||||
|
self.audio_queue = audio_queue
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while not stop_event.is_set():
|
||||||
|
try:
|
||||||
|
rel, v, index = self.video_queue.get(timeout=1)
|
||||||
|
except Empty:
|
||||||
|
continue
|
||||||
|
|
||||||
|
audio_path = f"/tmp/mt_{rel.replace('/', '_')}.wav"
|
||||||
|
try:
|
||||||
|
if video_hash(v["path"]) != v["hash"]:
|
||||||
|
log(f" SKIP {rel} - file changed")
|
||||||
|
continue
|
||||||
|
log(f" [CPU] Extract: {rel}")
|
||||||
|
subprocess.run(
|
||||||
|
["ffmpeg", "-y", "-i", v["path"], "-vn", "-acodec", "pcm_s16le",
|
||||||
|
"-ar", "16000", "-ac", "1", audio_path],
|
||||||
|
check=True, timeout=300, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
||||||
|
)
|
||||||
|
self.audio_queue.put((rel, v, audio_path, index))
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
log(f" [CPU] Extract timeout: {rel}")
|
||||||
|
except Exception as e:
|
||||||
|
log(f" [CPU] Extract FAIL {rel}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
class InferWorker:
|
||||||
|
def __init__(self, audio_queue, result_queue):
|
||||||
|
self.audio_queue = audio_queue
|
||||||
|
self.result_queue = result_queue
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while not stop_event.is_set():
|
||||||
|
try:
|
||||||
|
item = self.audio_queue.get(timeout=1)
|
||||||
|
except Empty:
|
||||||
|
continue
|
||||||
|
if item is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
rel, v, audio_path, index = item
|
||||||
|
dur = v.get("duration_seconds") or 120
|
||||||
|
vid_timeout = max(int(dur * SPEED_FACTOR) + 60, 120)
|
||||||
|
|
||||||
|
try:
|
||||||
|
log(f" [GPU] Transcribe: {rel} (est {dur * SPEED_FACTOR:.0f}s)")
|
||||||
|
resp = subprocess.run(
|
||||||
|
["curl", "-s", "--max-time", str(vid_timeout),
|
||||||
|
f"{WHISPER_SERVER}/inference",
|
||||||
|
"-F", f'file=@"{audio_path}"',
|
||||||
|
"-F", "response_format=srt"],
|
||||||
|
capture_output=True, text=True, timeout=vid_timeout + 30
|
||||||
|
)
|
||||||
|
if resp.returncode != 0:
|
||||||
|
raise RuntimeError(f"whisper-server error: {resp.stderr}")
|
||||||
|
if not resp.stdout.strip():
|
||||||
|
raise RuntimeError("empty result")
|
||||||
|
self.result_queue.put((rel, resp.stdout, index))
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
log(f" [GPU] Timeout: {rel}")
|
||||||
|
except Exception as e:
|
||||||
|
log(f" [GPU] FAIL {rel}: {e}")
|
||||||
|
finally:
|
||||||
|
if os.path.exists(audio_path):
|
||||||
|
os.remove(audio_path)
|
||||||
|
|
||||||
|
|
||||||
|
class SaveWorker:
|
||||||
|
def __init__(self, result_queue, stats):
|
||||||
|
self.result_queue = result_queue
|
||||||
|
self.stats = stats
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while not stop_event.is_set():
|
||||||
|
try:
|
||||||
|
item = self.result_queue.get(timeout=1)
|
||||||
|
except Empty:
|
||||||
|
continue
|
||||||
|
if item is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
rel, srt, index = item
|
||||||
|
try:
|
||||||
|
video_path = index["videos"][rel]["path"]
|
||||||
|
parent = os.path.dirname(video_path)
|
||||||
|
trans_dir = os.path.join(parent, "_transcriptions")
|
||||||
|
base = os.path.splitext(os.path.basename(video_path))[0]
|
||||||
|
out = os.path.join(trans_dir, f"{base}_transcription.srt")
|
||||||
|
os.makedirs(trans_dir, exist_ok=True)
|
||||||
|
with open(out, "w") as f:
|
||||||
|
f.write(srt)
|
||||||
|
|
||||||
|
index["videos"][rel]["transcribed"] = True
|
||||||
|
index["videos"][rel]["transcript_file"] = out
|
||||||
|
index["videos"][rel]["transcribed_at"] = datetime.now(timezone.utc).isoformat()
|
||||||
|
index["transcribed"] += 1
|
||||||
|
save_index(index)
|
||||||
|
self.stats["done"] += 1
|
||||||
|
log(f" [SAVE] OK {rel}")
|
||||||
|
except Exception as e:
|
||||||
|
log(f" [SAVE] FAIL {rel}: {e}")
|
||||||
|
self.stats["failed"] += 1
|
||||||
|
|
||||||
|
|
||||||
|
def thermal_sleep(seconds, label):
|
||||||
|
"""Sleep with progress logging. Respects stop_event."""
|
||||||
|
for i in range(seconds, 0, -30):
|
||||||
|
if stop_event.is_set():
|
||||||
|
break
|
||||||
|
log(f" [{label}] Sleeping... {i}s remaining")
|
||||||
|
stop_event.wait(30)
|
||||||
|
|
||||||
|
|
||||||
|
def run_continuous():
|
||||||
|
"""Run 24/7 with thermal breaks: WORK_SECONDS on, REST_SECONDS off."""
|
||||||
|
log(f"Continuous mode: model={WHISPER_MODEL}, server={WHISPER_SERVER}")
|
||||||
|
log(f"Thermal cycle: {WORK_SECONDS}s work / {REST_SECONDS}s rest")
|
||||||
|
log(f"Speed factor: {SPEED_FACTOR}x")
|
||||||
|
|
||||||
|
cycle = 0
|
||||||
|
while not stop_event.is_set():
|
||||||
|
cycle += 1
|
||||||
|
log(f"=== Cycle {cycle}: WORK phase ({WORK_SECONDS}s) ===")
|
||||||
|
|
||||||
|
# Re-scan index each cycle to pick up new files
|
||||||
|
index = load_index()
|
||||||
|
pending = [(rel, v) for rel, v in index["videos"].items() if not v["transcribed"]]
|
||||||
|
|
||||||
|
if not pending:
|
||||||
|
log("All videos transcribed! Exiting.")
|
||||||
|
return
|
||||||
|
|
||||||
|
log(f"Pending: {len(pending)} videos")
|
||||||
|
|
||||||
|
# Queues
|
||||||
|
video_queue = Queue(maxsize=2)
|
||||||
|
audio_queue = Queue(maxsize=2)
|
||||||
|
result_queue = Queue(maxsize=4)
|
||||||
|
stats = {"done": 0, "failed": 0}
|
||||||
|
|
||||||
|
# Start pipeline FIRST (consumers need to run before we enqueue)
|
||||||
|
threads = []
|
||||||
|
threads.append(threading.Thread(target=ExtractWorker(video_queue, audio_queue).run, daemon=True))
|
||||||
|
threads.append(threading.Thread(target=InferWorker(audio_queue, result_queue).run, daemon=True))
|
||||||
|
threads.append(threading.Thread(target=SaveWorker(result_queue, stats).run, daemon=True))
|
||||||
|
for t in threads:
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
# Enqueue eligible videos
|
||||||
|
enqueued = 0
|
||||||
|
for rel, v in pending:
|
||||||
|
dur = v.get("duration_seconds") or 120
|
||||||
|
est = dur * SPEED_FACTOR
|
||||||
|
if est <= WORK_SECONDS:
|
||||||
|
video_queue.put((rel, v, index))
|
||||||
|
enqueued += 1
|
||||||
|
log(f"Enqueued for this cycle: {enqueued}")
|
||||||
|
|
||||||
|
# Monitor work phase
|
||||||
|
phase_start = time.time()
|
||||||
|
last_done = 0
|
||||||
|
while True:
|
||||||
|
time.sleep(5)
|
||||||
|
if stop_event.is_set():
|
||||||
|
break
|
||||||
|
|
||||||
|
elapsed = time.time() - phase_start
|
||||||
|
if elapsed >= WORK_SECONDS:
|
||||||
|
log(f"Work phase done ({elapsed:.0f}s). {stats['done']} transcribed.")
|
||||||
|
break
|
||||||
|
|
||||||
|
if stats["done"] > last_done:
|
||||||
|
last_done = stats["done"]
|
||||||
|
|
||||||
|
stop_event.set()
|
||||||
|
for t in threads:
|
||||||
|
t.join(timeout=3)
|
||||||
|
|
||||||
|
total_before = index["transcribed"] - stats["done"] - stats["failed"]
|
||||||
|
log(f"Cycle {cycle} result: {stats['done']} done, {stats['failed']} failed")
|
||||||
|
|
||||||
|
# Thermal break
|
||||||
|
remaining = load_index()["total_videos"] - load_index()["transcribed"]
|
||||||
|
if remaining <= 0:
|
||||||
|
log("All videos transcribed! Done.")
|
||||||
|
return
|
||||||
|
|
||||||
|
log(f"=== Cycle {cycle}: REST phase ({REST_SECONDS}s) - {remaining} remaining ===")
|
||||||
|
stop_event.clear()
|
||||||
|
thermal_sleep(REST_SECONDS, "REST")
|
||||||
|
stop_event.set()
|
||||||
|
|
||||||
|
|
||||||
|
def run_once():
|
||||||
|
"""Single-shot batch (legacy mode)."""
|
||||||
|
log(f"Single batch: model={WHISPER_MODEL}, server={WHISPER_SERVER}")
|
||||||
|
|
||||||
|
index = load_index()
|
||||||
|
pending = [(rel, v) for rel, v in index["videos"].items() if not v["transcribed"]]
|
||||||
|
if not pending:
|
||||||
|
log("No pending videos.")
|
||||||
|
return
|
||||||
|
|
||||||
|
log(f"Found {len(pending)} untranscribed videos")
|
||||||
|
|
||||||
|
video_queue = Queue(maxsize=2)
|
||||||
|
audio_queue = Queue(maxsize=2)
|
||||||
|
result_queue = Queue(maxsize=4)
|
||||||
|
stats = {"done": 0, "failed": 0}
|
||||||
|
|
||||||
|
for rel, v in pending:
|
||||||
|
video_queue.put((rel, v, index))
|
||||||
|
|
||||||
|
threads = []
|
||||||
|
threads.append(threading.Thread(target=ExtractWorker(video_queue, audio_queue).run, daemon=True))
|
||||||
|
threads.append(threading.Thread(target=InferWorker(audio_queue, result_queue).run, daemon=True))
|
||||||
|
threads.append(threading.Thread(target=SaveWorker(result_queue, stats).run, daemon=True))
|
||||||
|
for t in threads:
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
while not video_queue.empty() or not audio_queue.empty() or not result_queue.empty():
|
||||||
|
time.sleep(5)
|
||||||
|
elapsed = time.time() - start
|
||||||
|
if elapsed >= WORK_SECONDS + REST_SECONDS:
|
||||||
|
log(f"Timeout ({elapsed:.0f}s). {stats['done']} done.")
|
||||||
|
stop_event.set()
|
||||||
|
break
|
||||||
|
stop_event.set()
|
||||||
|
for t in threads:
|
||||||
|
t.join(timeout=3)
|
||||||
|
|
||||||
|
elapsed = time.time() - start
|
||||||
|
log(f"Done: {stats['done']} transcribed, {stats['failed']} failed, {elapsed:.0f}s")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cmd = sys.argv[1] if len(sys.argv) > 1 else "continuous"
|
||||||
|
if cmd == "scan":
|
||||||
|
skip_dur = "--duration" not in sys.argv
|
||||||
|
idx = scan(skip_duration=skip_dur)
|
||||||
|
print(json.dumps({
|
||||||
|
"total": idx["total_videos"],
|
||||||
|
"transcribed": idx["transcribed"],
|
||||||
|
"pending": idx["total_videos"] - idx["transcribed"],
|
||||||
|
}, indent=2))
|
||||||
|
elif cmd == "continuous":
|
||||||
|
run_continuous()
|
||||||
|
elif cmd == "transcribe":
|
||||||
|
run_once()
|
||||||
|
else:
|
||||||
|
print(f"Usage: {sys.argv[0]} [scan|transcribe|continuous]")
|
||||||
|
sys.exit(1)
|
||||||
19
systemd/media-transcriber.service
Normal file
19
systemd/media-transcriber.service
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Media Transcriber Continuous Worker
|
||||||
|
Requires=whisper-server.service
|
||||||
|
After=whisper-server.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=jarian
|
||||||
|
WorkingDirectory=/home/jarian/projects/media-transcriber
|
||||||
|
ExecStart=/home/jarian/projects/media-transcriber/venv/bin/python \
|
||||||
|
/home/jarian/projects/media-transcriber/src/worker.py continuous
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=60
|
||||||
|
Environment=WORK_SECONDS=2700
|
||||||
|
Environment=REST_SECONDS=900
|
||||||
|
Environment=WHISPER_MODEL=base.en
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
18
systemd/whisper-server.service
Normal file
18
systemd/whisper-server.service
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Whisper.cpp Transcription Server
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=jarian
|
||||||
|
ExecStart=/home/jarian/whisper.cpp/build/bin/whisper-server \
|
||||||
|
-m /home/jarian/whisper.cpp/models/ggml-base.en.bin \
|
||||||
|
--host 127.0.0.1 \
|
||||||
|
--port 8888 \
|
||||||
|
--convert
|
||||||
|
Environment=CUDA_VISIBLE_DEVICES=1
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Loading…
x
Reference in New Issue
Block a user