yt-dlf/scripts/transcribe.py
Claude Fable 5 a3eb962b90 Transcribe videos without subtitles via faster-whisper fallback
When yt-dlp yields no subtitle files, the server now runs
scripts/transcribe.py (faster-whisper, large-v3-turbo, CUDA with CPU
fallback) on the downloaded media and feeds the resulting VTT through
the existing Markdown conversion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1w53ybPWArpZpcei9JPYX
2026-08-29 22:20:55 +02:00

104 lines
3.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Transcribe a media file to WebVTT using faster-whisper.
Usage: transcribe.py <mediafile>
The VTT is written next to the input file as <base>.<detected-lang>.vtt
and its path is printed as a final "TRANSCRIPT_VTT:<path>" line so the
calling server can pick it up.
Environment variables:
WHISPER_MODEL faster-whisper model name (default: large-v3-turbo)
WHISPER_DEVICE "cuda" or "cpu" (default: try cuda, fall back to cpu)
Exit codes: 0 ok, 2 usage/input error, 3 faster-whisper not installed.
"""
import ctypes
import glob
import os
import sys
def preload_nvidia_libs():
"""Load pip-installed cuBLAS/cuDNN so ctranslate2 finds them without
LD_LIBRARY_PATH (dlopen by soname succeeds once they are in-process)."""
site_dirs = [d for d in sys.path if d.endswith("site-packages")]
for site in site_dirs:
for lib in sorted(glob.glob(os.path.join(site, "nvidia", "*", "lib", "*.so*"))):
try:
ctypes.CDLL(lib, mode=ctypes.RTLD_GLOBAL)
except OSError:
pass
def fmt_ts(seconds):
ms = int(round(seconds * 1000))
h, ms = divmod(ms, 3_600_000)
m, ms = divmod(ms, 60_000)
s, ms = divmod(ms, 1000)
return f"{h:02d}:{m:02d}:{s:02d}.{ms:03d}"
def main():
if len(sys.argv) != 2:
print(__doc__.strip(), file=sys.stderr)
return 2
media = sys.argv[1]
if not os.path.isfile(media):
print(f"Input file not found: {media}", file=sys.stderr)
return 2
preload_nvidia_libs()
try:
from faster_whisper import WhisperModel
except ImportError:
print("faster-whisper is not installed in this Python environment", file=sys.stderr)
return 3
model_name = os.environ.get("WHISPER_MODEL", "large-v3-turbo")
device = os.environ.get("WHISPER_DEVICE")
attempts = [(device, None)] if device else [("cuda", "float16"), ("cpu", "int8")]
model = None
for dev, compute in attempts:
try:
model = WhisperModel(model_name, device=dev, compute_type=compute or "default")
print(f"Whisper model {model_name} loaded on {dev}", flush=True)
break
except Exception as err:
print(f"Could not load model on {dev}: {err}", file=sys.stderr, flush=True)
if model is None:
return 1
segments, info = model.transcribe(media, vad_filter=True)
print(f"Detected language: {info.language} "
f"(p={info.language_probability:.2f}), duration {fmt_ts(info.duration)}", flush=True)
base, _ = os.path.splitext(media)
vtt_path = f"{base}.{info.language}.vtt"
lines = ["WEBVTT", ""]
for seg in segments:
text = seg.text.strip()
if not text:
continue
lines.append(f"{fmt_ts(seg.start)} --> {fmt_ts(seg.end)}")
lines.append(text)
lines.append("")
# progress for the live log (segments arrive lazily during transcription)
print(f"[transcribe] {fmt_ts(seg.start)} {text}", flush=True)
if len(lines) <= 2:
print("No speech detected — no transcript written", flush=True)
return 0
with open(vtt_path, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
print(f"TRANSCRIPT_VTT:{vtt_path}", flush=True)
return 0
if __name__ == "__main__":
sys.exit(main())