Whisper Transcriber
audio / v1.0.1
Find audio files without transcripts and transcribe them with Whisper.
Recursively scans a directory for supported audio files, skips files that already have a matching Whisper transcript, loads the selected Whisper model once, and transcribes only unfinished recordings. Supports TXT, VTT, SRT, TSV, and JSON output, optional language selection, custom audio extensions, dry-run mode, and a shared output directory.
Requirements
- Python 3.9+
- Python package: openai-whisper
- ffmpeg
Platforms
LinuxMacOSWindows
Usage
python3 whisper_transcribe.py ./recordings
python3 whisper_transcribe.py ./recordings --model large-v3
python3 whisper_transcribe.py ./recordings --language en --output_format txt
python3 whisper_transcribe.py ./recordings --formats mp3,m4a,wav --dry-run
Download
Install quickly or copy a command for your shell.
toolbox whisper-transcribe
curl -fsSL "https://raw.githubusercontent.com/PiSaucer/toolbox/be1681a03e94be2097cb59fe632ba5e702015d1c/scripts/whisper_transcribe.py" -o "whisper_transcribe.py"
wget -O "whisper_transcribe.py" "https://raw.githubusercontent.com/PiSaucer/toolbox/be1681a03e94be2097cb59fe632ba5e702015d1c/scripts/whisper_transcribe.py"
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/PiSaucer/toolbox/be1681a03e94be2097cb59fe632ba5e702015d1c/scripts/whisper_transcribe.py" -OutFile "whisper_transcribe.py"
python3 -c "import urllib.request; urllib.request.urlretrieve('https://raw.githubusercontent.com/PiSaucer/toolbox/be1681a03e94be2097cb59fe632ba5e702015d1c/scripts/whisper_transcribe.py', 'whisper_transcribe.py')"
Integrity
SHA256
153d73c0bc49660a30038aba5810e298b357d2f6f309ce152ce164660bf1fa1e
Copy and Paste Script
Use this when you want to copy the full script directly.
#!/usr/bin/env python3
# whisper_transcribe.py
# Copyright (c) 2026 PiSaucer
# Licensed under the MIT License
# Version 1.0.1
# Find audio files without Whisper transcripts and transcribe the missing files.
# Usage: python3 whisper_transcribe.py DIRECTORY [options]
import argparse
import sys
from pathlib import Path
from typing import List, Set, Optional
# Required package: python3 -m pip install openai-whisper
import whisper
VERSION = "1.0.1"
DEFAULT_FORMATS = {
".aac",
".flac",
".m4a",
".mp3",
".ogg",
".opus",
".wav",
".wma",
}
OUTPUT_FORMATS = (
"txt",
"vtt",
"srt",
"tsv",
"json",
)
ALL_FORMATS = "all"
def parse_formats(value: str) -> Set[str]:
"""Parse a comma-separated list of audio file extensions.
Args:
value: Comma-separated extensions such as ``mp3,m4a,wav``.
Returns:
Normalized lowercase extensions including the leading dot.
Raises:
argparse.ArgumentTypeError: If no valid extensions are supplied.
"""
formats = set()
for item in value.split(","):
extension = item.strip().lower()
if not extension:
continue
if not extension.startswith("."):
extension = f".{extension}"
formats.add(extension)
if not formats:
raise argparse.ArgumentTypeError("at least one audio format is required")
return formats
def find_audio_files(directory: Path, formats: Set[str], recursive: bool = True) -> List[Path]:
"""Find supported audio files below a directory.
Args:
directory: Directory to search.
formats: Accepted lowercase file extensions.
recursive: Whether to search nested directories.
Returns:
Matching audio files sorted case-insensitively by path.
Raises:
FileNotFoundError: If the directory does not exist.
ValueError: If the input path is not a directory.
OSError: If the directory cannot be searched.
"""
if not directory.exists():
raise FileNotFoundError(f"directory not found: {directory}")
if not directory.is_dir():
raise ValueError(f"input path is not a directory: {directory}")
iterator = directory.rglob("*") if recursive else directory.glob("*")
files = [path for path in iterator if path.is_file() and path.suffix.lower() in formats]
return sorted(files, key=lambda path: str(path).lower())
def transcript_path(audio_file: Path, output_format: str, output_dir: Optional[Path] = None) -> Path:
"""Determine the expected transcript path for an audio file.
Args:
audio_file: Source audio file.
output_format: Whisper output format.
output_dir: Optional common output directory.
Returns:
Expected transcript path.
"""
filename = f"{audio_file.stem}.{output_format}"
if output_dir is not None:
return output_dir / filename
return audio_file.parent / filename
def transcript_paths(audio_file: Path, output_format: str, output_dir: Optional[Path] = None) -> List[Path]:
"""Return the expected transcript path(s) for a Whisper output format."""
formats = OUTPUT_FORMATS if output_format == ALL_FORMATS else (output_format,)
return [transcript_path(audio_file, fmt, output_dir) for fmt in formats]
def find_missing_transcripts(audio_files: List[Path], output_format: str, output_dir: Optional[Path] = None) -> List[Path]:
"""Find audio files whose expected transcript does not exist.
Args:
audio_files: Audio files to inspect.
output_format: Whisper output format.
output_dir: Optional common transcript directory.
Returns:
Audio files without a matching transcript.
"""
return [
audio_file
for audio_file in audio_files
if not all(path.is_file() for path in transcript_paths(audio_file, output_format, output_dir))
]
def transcribe_audio(
model,
audio_file: Path,
output_format: str,
output_dir: Optional[Path] = None,
language: Optional[str] = None,
) -> Path:
"""Transcribe one audio file with Whisper.
Args:
model: Loaded Whisper model.
audio_file: Audio file to transcribe.
output_format: Transcript output format.
output_dir: Optional common output directory.
language: Optional Whisper language code.
Returns:
Path to the generated transcript.
Raises:
OSError: If output files cannot be written.
RuntimeError: If Whisper transcription fails.
"""
destination = output_dir if output_dir is not None else audio_file.parent
destination.mkdir(parents=True, exist_ok=True)
options = {}
if language:
options["language"] = language
result = model.transcribe(str(audio_file), **options)
writer = whisper.utils.get_writer(output_format, str(destination))
writer(result, str(audio_file))
return transcript_path(audio_file, output_format, output_dir)
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments.
Returns:
Parsed directory, Whisper model, audio formats, and output options.
Raises:
SystemExit: If arguments are invalid or argparse handles an immediate
action such as ``--help`` or ``--version``.
"""
parser = argparse.ArgumentParser(description=("Find audio files without matching Whisper transcripts and transcribe the missing files."))
parser.add_argument(
"directory",
type=Path,
help="directory containing audio files",
)
parser.add_argument(
"-m",
"--model",
default="turbo",
help="Whisper model to use (default: turbo)",
)
parser.add_argument(
"-l",
"--language",
help="audio language code such as en (default: auto-detect)",
)
parser.add_argument(
"-o",
"--output-dir",
type=Path,
help="transcript output directory (default: beside each audio file)",
)
parser.add_argument(
"--output_format",
"--output-format",
dest="output_format",
choices=(*OUTPUT_FORMATS, ALL_FORMATS),
default=ALL_FORMATS,
help="Whisper output format (default: all)",
)
parser.add_argument(
"--formats",
type=parse_formats,
default=DEFAULT_FORMATS,
metavar="FORMATS",
help=(
"comma-separated audio extensions "
"(default: aac,flac,m4a,mp3,ogg,opus,wav,wma)"
),
)
parser.add_argument(
"--no-recursive",
action="store_true",
help="do not search subdirectories",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="list missing transcripts without running Whisper",
)
parser.add_argument(
"--version",
action="version",
version=f"%(prog)s {VERSION}",
)
return parser.parse_args()
def main() -> int:
"""Find missing transcripts and run Whisper on each unfinished file.
Returns:
Zero on success or one when validation, transcription, model loading,
or file I/O fails.
"""
args = parse_args()
directory = args.directory.expanduser()
output_dir = args.output_dir.expanduser() if args.output_dir else None
try:
audio_files = find_audio_files(directory, args.formats, recursive=not args.no_recursive)
missing_files = find_missing_transcripts(audio_files, args.output_format, output_dir)
completed = len(audio_files) - len(missing_files)
print(f"Found {len(audio_files)} audio file(s)")
print(f"Already transcribed: {completed}")
print(f"Missing transcripts: {len(missing_files)}")
if not missing_files:
print("No transcription needed.")
return 0
if args.dry_run:
print()
for audio_file in missing_files:
print(audio_file)
print()
print("Dry run enabled: no files were transcribed.")
return 0
print(f"Loading Whisper model: {args.model}")
model = whisper.load_model(args.model)
completed_count = 0
failed_count = 0
for index, audio_file in enumerate(missing_files, start=1):
print()
print(f"[{index}/{len(missing_files)}] Transcribing: {audio_file}")
try:
output_file = transcribe_audio(model, audio_file, args.output_format, output_dir, args.language)
except Exception as error:
failed_count += 1
print(
f"Error transcribing {audio_file}: {error}",
file=sys.stderr,
)
continue
completed_count += 1
print(f"Transcript: {output_file}")
except (OSError, RuntimeError, ValueError) as error:
print(f"Error: {error}", file=sys.stderr)
return 1
print()
print(f"Completed: {completed_count} file(s) transcribed, \n{failed_count} failed, {completed} skipped")
return 1 if failed_count else 0
if __name__ == "__main__":
raise SystemExit(main())