119 lines
3.6 KiB
Python
119 lines
3.6 KiB
Python
"""Batch-transcribe every video in a directory with 4 parallel workers.
|
|
|
|
Walks <videos_dir> for common video extensions, runs ElevenLabs Scribe on
|
|
each, writes transcripts to <videos_dir>/edit/transcripts/<name>.json.
|
|
|
|
Cached per-file: any source that already has a transcript is skipped.
|
|
|
|
Usage:
|
|
python helpers/transcribe_batch.py <videos_dir>
|
|
python helpers/transcribe_batch.py <videos_dir> --workers 4
|
|
python helpers/transcribe_batch.py <videos_dir> --num-speakers 2
|
|
python helpers/transcribe_batch.py <videos_dir> --edit-dir /custom/edit
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
import time
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
from pathlib import Path
|
|
|
|
from transcribe import load_api_key, transcribe_one
|
|
|
|
|
|
VIDEO_EXTS = {".mp4", ".MP4", ".mov", ".MOV", ".mkv", ".MKV", ".avi", ".AVI", ".m4v"}
|
|
|
|
|
|
def find_videos(videos_dir: Path) -> list[Path]:
|
|
videos = sorted(
|
|
p for p in videos_dir.iterdir()
|
|
if p.is_file() and p.suffix in VIDEO_EXTS
|
|
)
|
|
return videos
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser(description="Parallel batch transcription of a videos directory")
|
|
ap.add_argument("videos_dir", type=Path, help="Directory containing source videos")
|
|
ap.add_argument(
|
|
"--edit-dir",
|
|
type=Path,
|
|
default=None,
|
|
help="Edit output directory (default: <videos_dir>/edit)",
|
|
)
|
|
ap.add_argument("--workers", type=int, default=4, help="Parallel workers (default: 4)")
|
|
ap.add_argument(
|
|
"--language",
|
|
type=str,
|
|
default=None,
|
|
help="Optional ISO language code. Omit to auto-detect per file.",
|
|
)
|
|
ap.add_argument(
|
|
"--num-speakers",
|
|
type=int,
|
|
default=None,
|
|
help="Optional number of speakers. Improves diarization when known.",
|
|
)
|
|
args = ap.parse_args()
|
|
|
|
videos_dir = args.videos_dir.resolve()
|
|
if not videos_dir.is_dir():
|
|
sys.exit(f"not a directory: {videos_dir}")
|
|
|
|
edit_dir = (args.edit_dir or (videos_dir / "edit")).resolve()
|
|
(edit_dir / "transcripts").mkdir(parents=True, exist_ok=True)
|
|
|
|
videos = find_videos(videos_dir)
|
|
if not videos:
|
|
sys.exit(f"no videos found in {videos_dir}")
|
|
|
|
already_cached = [v for v in videos if (edit_dir / "transcripts" / f"{v.stem}.json").exists()]
|
|
pending = [v for v in videos if v not in already_cached]
|
|
|
|
print(f"found {len(videos)} videos ({len(already_cached)} cached, {len(pending)} to transcribe)")
|
|
if not pending:
|
|
print("nothing to do")
|
|
return
|
|
|
|
api_key = load_api_key()
|
|
|
|
print(f"transcribing {len(pending)} files with {args.workers} parallel workers")
|
|
t0 = time.time()
|
|
|
|
errors: list[tuple[Path, str]] = []
|
|
with ThreadPoolExecutor(max_workers=args.workers) as pool:
|
|
futures = {
|
|
pool.submit(
|
|
transcribe_one,
|
|
video=v,
|
|
edit_dir=edit_dir,
|
|
api_key=api_key,
|
|
language=args.language,
|
|
num_speakers=args.num_speakers,
|
|
verbose=False,
|
|
): v
|
|
for v in pending
|
|
}
|
|
for fut in as_completed(futures):
|
|
v = futures[fut]
|
|
try:
|
|
out = fut.result()
|
|
print(f" + {v.stem} → {out.name}")
|
|
except Exception as e:
|
|
errors.append((v, str(e)))
|
|
print(f" x {v.stem} FAILED: {e}")
|
|
|
|
dt = time.time() - t0
|
|
print(f"\ndone in {dt:.1f}s")
|
|
if errors:
|
|
print(f"{len(errors)} failures:")
|
|
for v, msg in errors:
|
|
print(f" {v.name}: {msg}")
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|