Pricing Dashboard Sign up
Recent
· 9 min read · MDisBetter

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:

Setting up Whisper locally

Prerequisites

Install Whisper

pip install openai-whisper

That'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 whisperx

WhisperX 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.content

Call 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:

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:

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')
        continue

Don'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:

  1. 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.
  2. Non-technical users. Setting up Python, FFmpeg, and Hugging Face tokens is a real friction barrier.
  3. 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.

Frequently asked questions

Does mdisbetter offer an API or CLI for batch audio transcription?
No. The only surface is the web tool at /convert/audio-to-markdown. It accepts several files at once in the browser, but it cannot be scripted or scheduled. For automation and large batches, the right path is open-source Whisper or WhisperX running locally on your machine. Both are MIT-licensed and a few pip-installs away. The recipes above are working starting points.
Can I run Whisper on a Mac without a GPU?
Yes, a supported Whisper implementation can use CPU processing on a Mac. Check its documented device backend instead of assuming it uses Apple Neural Engine acceleration. Run a representative file, observe memory and elapsed time, then choose a model that your machine can sustain.
What's the file size limit on local Whisper?
Check the chosen implementation and its audio-decoding requirements. For long files, monitor memory, disk space and completion on a representative input. Retain a visible failure log and compare the output ending with the source; processing in internal windows does not remove every practical resource limit.