Batch Transcribe Multiple Audio Files at Once
For a backlog of recordings, choose a web upload batch or a local script that manages recognition and file outputs. MDisBetter accepts several audio files in its signed-in browser tool, with credits used for each conversion. For unattended local processing, open-source Whisper is an option to evaluate. Installation, model downloads, compute and review still require work; start with a checked sample before queueing the collection.
The honest disclosure
The mdisbetter audio-to-markdown web tool is a browser interface. You can select several audio files at once; each one is transcribed as its own conversion (and uses credits) and comes back as its own Markdown file. There is no API, no CLI, no SDK, and nothing you can schedule or leave running unattended. For high-volume or recurring work, compare the local script's setup, resource use and review effort with the signed-in web workflow before choosing how to process the queue.
Open-source Whisper and faster-whisper can run recognition on your machine and let a script manage the file queue. This avoids a hosted per-file transcription invoice, but installation, model downloads, compute, storage and human review remain costs. Compare the local and web results on the same source passages before choosing a path; neither is assumed equivalent or more accurate.
Practical thresholds:
- A backlog you can upload in one sitting: web tool, several files at a time. You select the files, wait, and download one transcript per file.
- A large file queue: test the local script on a representative subset, reconcile accepted outputs with source filenames and failures, and plan compute and correction work from that pilot. The absence of a hosted recognition bill does not eliminate setup or review effort.
- A recurring stream, or anything that must run unattended: local Whisper on a schedule is the only option, since the web tool needs you in the browser.
Setting up Whisper locally
Prerequisites
- Python 3.9 or newer
- FFmpeg installed and on your PATH (audio decoding)
- A reasonably modern machine. A GPU helps but isn't required for moderate batches; large batches benefit significantly from CUDA-capable hardware.
Install Whisper
pip install openai-whisperThat's the entire installation. The model weights download automatically on first use.
Test on a single file
whisper test.m4a --model large-v3 --output_format txt --output_dir transcripts/The first run downloads the model (a few GB for large-v3). Subsequent runs use the cached weights. Output is a test.txt file in the transcripts/ directory (the Whisper command line has no Markdown format; the scripts below write .md files directly).
The basic batch script
For straightforward batch processing of all audio files in a folder:
import whisper
from pathlib import Path
model = whisper.load_model("large-v3")
audio_dir = Path("./audio-inbox")
output_dir = Path("./transcripts")
output_dir.mkdir(exist_ok=True)
extensions = {".mp3", ".m4a", ".wav", ".ogg", ".flac", ".mp4"}
for audio_file in audio_dir.iterdir():
if audio_file.suffix.lower() not in extensions:
continue
md_path = output_dir / f"{audio_file.name}.md"
if md_path.exists():
print(f"SKIP {audio_file.name} (already transcribed)")
continue
print(f"PROCESSING {audio_file.name}...")
result = model.transcribe(str(audio_file), verbose=False)
md_path.write_text(
f"# {audio_file.stem}\n\n{result['text']}\n",
encoding="utf-8"
)
print(f"OK {md_path}")
print("Done.")This is the simplest version. It produces a single Markdown file per audio with the full transcript as flowing text, no speaker labels, no section headings.
Adding speaker diarization with WhisperX
For interviews, meetings, or any multi-speaker content, you want speaker labels. Plain Whisper doesn't include diarization; WhisperX adds it.
Install WhisperX
pip install whisperxWhisperX requires a Hugging Face token (free) for the diarization models. Sign up at huggingface.co, generate a read token in your account settings, and accept the terms for the pyannote/speaker-diarization-3.1 model.
Batch script with diarization
import whisperx
from pathlib import Path
import os
DEVICE = "cuda" if whisperx.utils.torch.cuda.is_available() else "cpu"
COMPUTE_TYPE = "float16" if DEVICE == "cuda" else "int8"
HF_TOKEN = os.environ["HF_TOKEN"] # set this in your shell
model = whisperx.load_model("large-v3", DEVICE, compute_type=COMPUTE_TYPE)
diarize_model = whisperx.DiarizationPipeline(use_auth_token=HF_TOKEN, device=DEVICE)
audio_dir = Path("./audio-inbox")
output_dir = Path("./transcripts")
output_dir.mkdir(exist_ok=True)
extensions = {".mp3", ".m4a", ".wav", ".ogg", ".flac", ".mp4"}
for audio_file in audio_dir.iterdir():
if audio_file.suffix.lower() not in extensions:
continue
md_path = output_dir / f"{audio_file.name}.md"
if md_path.exists():
print(f"SKIP {audio_file.name}: existing output {md_path.name}")
continue
print(f"PROCESSING {audio_file.name}...")
audio = whisperx.load_audio(str(audio_file))
result = model.transcribe(audio, batch_size=16)
# alignment
align_model, metadata = whisperx.load_align_model(
language_code=result["language"], device=DEVICE
)
result = whisperx.align(
result["segments"], align_model, metadata, audio, DEVICE
)
# diarization
diarize_segments = diarize_model(audio)
result = whisperx.assign_word_speakers(diarize_segments, result)
# write Markdown with speaker labels
lines = [f"# {audio_file.stem}\n"]
current_speaker = None
for segment in result["segments"]:
speaker = segment.get("speaker", "UNKNOWN")
if speaker != current_speaker:
lines.append(f"\n**{speaker}:** {segment['text'].strip()}")
current_speaker = speaker
else:
lines.append(segment["text"].strip())
md_path.write_text("\n".join(lines), encoding="utf-8")
print(f"OK {md_path}")This produces Markdown with bold speaker labels (**SPEAKER_00:**, **SPEAKER_01:**), which is something the web tool does not do: its output is a transcript split into timestamped segments, with no speaker identification. Run a find-and-replace pass after batch completion to rename the speakers per file.
Adding section headings with an LLM pass
Neither Whisper nor the web tool inserts section headings: both give you the transcript as it was spoken. If you want H2 headings at topic changes, add a second pass after transcription that uses an LLM to insert them.
import openai # or anthropic, or any other LLM client
def add_section_headings(transcript_md: str) -> str:
prompt = f"""Read this transcript. Insert H2 Markdown headings (## Topic) at natural topic shifts.
Rules:
- Insert headings only where the conversation clearly shifts to a new topic
- Headings should be substantive noun phrases (not 'Section 1' or 'New Topic')
- Preserve all existing content verbatim
- Do not add commentary or summary; just insert headings
Transcript:
{transcript_md}"""
response = openai.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
)
return response.choices[0].message.contentCall this on each transcribed file after the diarization pass. The headings make the long transcripts navigable.
This separate LLM step uses the selected provider's metered API and adds review work. Check the actual input, output and current billing for your configuration before estimating a batch; no per-file cost is established here.
Performance and hardware
Realistic transcription speed by hardware:
- CPU only: process one representative file before queueing the folder. Record elapsed time and inspect memory use and output. Use that result to decide whether to leave a batch running or choose another model.
- Mid-range GPU: confirm that the selected implementation detects the device and can load the chosen model. Run the same pilot file used for the CPU comparison, keeping model and decoding settings explicit.
- High-end GPU: check the implementation's actual batching behavior and peak memory use before increasing concurrency. More hardware does not by itself prove that several simultaneous jobs will finish efficiently.
- Apple Silicon: check which device backend your chosen implementation actually supports and uses. Benchmark that configuration instead of assuming every Whisper package uses the same acceleration path.
For batches under 50 files, even CPU-only is acceptable: leave it running while you do other work. For larger batches or production pipelines, GPU is worth the investment.
Memory considerations
The large-v3 model needs roughly 10GB of RAM (CPU) or 10GB of VRAM (GPU). For machines with less, fall back to medium (5GB) or small (2GB) models: accuracy is lower but still usable for many use cases.
Trade-off comparison on clean English audio:
large-v3: compare its output on the terminology and difficult passages in your corpus. Check that it loads on the intended hardware before scheduling a full batch.medium: compare the same reference passages with your larger-model result. Choose it when the observed runtime and correction work fit your workload, rather than assuming a fixed quality penalty.small: test names, numbers and overlapping speech as well as easy passages. Include the time needed to correct its transcript when assessing the practical benefit of the smaller model.tiny: test whether it produces enough usable text for your intended triage task. Keep important passages flagged for review; a quick draft should not be treated as an accepted final transcript.
Error handling for production batches
For batches of hundreds of files, robustness matters. Three patterns to add:
Skip-if-exists
Already in the basic script: check for the output .md before processing. Lets you re-run the script after partial failures without redoing work.
Try/except per file
import whisper
from pathlib import Path
model = whisper.load_model('large-v3')
audio_dir = Path('audio-inbox')
output_dir = Path('transcripts')
output_dir.mkdir(exist_ok=True)
extensions = {'.mp3', '.m4a', '.wav', '.ogg', '.flac', '.mp4'}
for audio_file in audio_dir.iterdir():
if not audio_file.is_file() or audio_file.suffix.lower() not in extensions:
continue
md_path = output_dir / (audio_file.name + '.md')
if md_path.exists():
print(f'SKIP {audio_file.name}: existing output {md_path.name}')
continue
try:
result = model.transcribe(str(audio_file), verbose=False)
md_path.write_text(result['text'], encoding='utf-8')
except Exception as error:
with (output_dir / 'failed.log').open('a', encoding='utf-8') as log:
log.write(f'{audio_file.name}\t{error}\n')
continueDon't let one corrupt file kill the whole batch. Log failures, continue, retry the failed list at the end.
Progress checkpointing
For very long batches (1000+ files), write progress to a state file so you can resume after interruption. A simple JSON file mapping audio filename → status (pending/done/failed) is sufficient.
Cross-feature: combining with PDF batch
If you have a corpus that includes both audio and PDFs (e.g., research interview project with consent forms and transcripts as PDFs), the parallel batch pipeline for PDFs uses similar tooling. See batch convert 100 PDFs to Markdown for that pipeline. Combined, you can produce a unified Markdown corpus across audio and document inputs.
Privacy: local-only processing
One of the biggest reasons to use the local Whisper path is privacy. The audio never leaves your machine. For sensitive content (legal, medical, HR, deeply personal), this is the right answer regardless of volume, even for single files. The recipe above runs offline once the model weights are cached.
When the web tool is still the right choice
Despite all of the above, the web tool wins in three cases:
- Ad-hoc conversions and small batches. The setup cost of the local pipeline isn't worth it for a single file or a folder you can upload in one go.
- Non-technical users. Setting up Python, FFmpeg, and Hugging Face tokens is a real friction barrier.
- Casual use that doesn't justify maintenance. A local pipeline needs occasional model updates, dependency updates, and machine maintenance.
For large or recurring batches, the local OSS path is the right answer.
Recommendation
For most users with batch transcription needs, the path is: install Whisper or WhisperX once, write the script once (start from the recipes above), and reuse it for every future batch. The setup is a few hours; the payback is essentially every batch you process from then on. The output Markdown integrates with the same downstream workflows (Obsidian, Notion, AI Q&A) as the web tool's output. The layout differs slightly (the web tool starts each segment with a timestamp in square brackets; the basic script above writes flowing text), but both are plain Markdown that those tools read the same way.
For occasional users who hit a one-time batch (clean out a backlog, transcribe a research project), the choice between web tool and local script is genuinely close. If the friction of installing tooling is high, upload the backlog to the web tool, several files at a time. For hundreds of files, the local script wins decisively.
One additional pattern: scheduled overnight processing
For teams accumulating recordings continuously (daily standups, weekly all-hands, ongoing customer interviews), a scheduled overnight batch is the lowest-effort production pattern. Drop new audio files into an inbox folder during the day; a scheduled job (cron on Linux/Mac, Task Scheduler on Windows) runs the batch script at 2am; transcripts appear in the output folder by morning.
The scheduling configuration is straightforward. On Linux/Mac, a cron entry like 0 2 * * * cd /path/to/project && /path/to/python script.py runs the script every night at 2am. On Windows, Task Scheduler does the equivalent through a GUI. Add a small notification step at the end of the script (push to a Slack webhook, email, or just a desktop notification) so you know overnight work completed and which files succeeded.
The pattern works because the batch script's skip-if-exists check makes it idempotent. The same script can run nightly without re-processing already-done files; it only handles whatever is new in the inbox folder. Combined with a simple intake convention (drop new audio in ./inbox/; transcripts appear in ./transcripts/ by morning) this becomes the lowest-friction production transcription pipeline for any team that produces recordings regularly.
One last note on accuracy and quality
Use the same acceptance checks for local and web transcripts: inspect names, numbers, ambiguous passages and the ending. Neither path supplies topic headings by default. The separate heading-generation recipe can produce a draft organization, but its headings and grouped passages still need review against the transcript.