MacTranscriber
A double-click app that turns a French or English recording into a text file on an Apple Silicon Mac. Once it is set up, it works offline.
I made this for my mom, who uses it to transcribe audio recordings. The speech recognition itself already exists, so the interesting problem was different: it had to work for someone who is never going to open a command line. I wanted it to be as easy to use as I could make it. She has used it.
You double-click one file, pick a recording, say whether it is in French or English and whether one person or two are talking, and press a button. A while later there is a text file next to the recording with everything that was said. The whole thing happens on the Mac. The recording is not sent anywhere.
The part I spent my time on is what happens to a long recording. The speech model I use is Whisper, from OpenAI, and I didn't write it. It is good, but if you hand it a few hours of audio in one go it can lose the thread and start repeating the same sentence over and over. It is a bit like a typist copying a very long tape without a break. So the app cuts the recording into pieces of about five minutes, and it cuts at the quietest moment it can find near each five-minute mark, so words are rarely sliced in half. Each piece is typed up separately, then the pieces are joined back together.
With two speakers, a second model listens to the same recording at the same time and works out when each voice is talking. The app then matches every word to a voice and labels the paragraphs "Speaker 1" and "Speaker 2", numbered in the order they first speak. It doesn't know names, and it always looks for exactly two voices.
Whisper also makes things up when the audio is silent or just music. It will write the sort of line that ends a subtitle file, like "thanks for watching". I keep a short list of those phrases and throw away any segment that contains one. I also throw away a line that would be the third identical one in a row, since that is usually the model stuck in a loop.
The first launch downloads the programs and speech models it needs, a few gigabytes in all, and needs the internet. After that it runs offline. There are two speeds: Fast, the one the app recommends, and Best, which uses a larger model, is slower, and downloads a few more gigabytes the first time you pick it. Nearly everything it installs lives in one folder, so you remove the app by deleting the folder. One small helper tool it installs, and some files it downloads, stay behind. The one rough spot is the very first start: macOS may refuse to open a file it doesn't recognise, and then you have to right-click it and choose Open, once.
It only does French and English, one or two speakers, and only on a Mac with an Apple chip. What I haven't done is time it on a long recording or measure how many mistakes it makes.
What's next. Put a real long recording through it and time it. Compare the output with a transcript I already know is right, and check how well it separates two voices speaking French.
Why, and what it is
I built this for my mom, who uses it to transcribe audio recordings. Whisper already does the hard part, so what I was working on was access: she doesn't use a command line, and the app had to be usable without one. That is why setup is a script that runs itself on the first double-click, why the window has only four choices, and why there is nothing to configure. It has been used, by her, for the thing it was made for.
You start it by double-clicking Transcriber.command, pick an audio or video file, choose French or English and one or two speakers, and a .txt appears next to the file. It never overwrites: if name.txt exists you get name 2.txt, and if the folder isn't writable the file goes in Documents.
The first launch downloads Python and the models, about 2 to 3 GB. After that it runs offline, except that choosing Best for the first time downloads about 3 GB more. All of it, models included, sits in one folder, so uninstalling is deleting the folder. Almost all of it. The launcher installs uv into ~/.local/bin if it isn't there, and uv keeps the Python it downloads, and its cache of downloaded packages, in its own places outside the folder, so deleting the folder leaves uv, that Python and that cache behind.
How it works
The window is the boring part. What matters is the path from a long recording to that text file, and it is one Python file, engine.py. Tap or click a box below to see what it does, or choose one speaker or two to see what changes.
Transcription runs on mlx-whisper. Fast uses whisper-large-v3-turbo. Best uses whisper-large-v3, the larger model, which I expect to be slower; I haven't timed the two against each other.
WHISPER_MODELS = {
"fast": "mlx-community/whisper-large-v3-turbo",
"best": "mlx-community/whisper-large-v3-mlx",
}
What is Whisper?
A speech-recognition model released by OpenAI, trained on a very large amount of audio in many languages. It takes sound in and gives text out, with a timestamp on every segment and, if you ask, on every word. I use it as it is and don't train or change it.
What is MLX?
Apple's array framework for the chips in recent Macs. The mlx-whisper package is Whisper rewritten to run on it, so the model runs on the Mac's GPU. That is why the app only runs on Apple Silicon.
Cutting at the quiet bits
Audio is decoded to mono 16 kHz with the ffmpeg binary that imageio-ffmpeg ships, so nothing has to be installed separately. A long file is then cut into chunks of about five minutes. Each cut goes at the quietest 100 ms frame within 15 seconds of the nominal boundary, so words are rarely split. I chunk because Whisper is prone to repetition loops on multi-hour audio. Each chunk is transcribed without the previous text as context (condition_on_previous_text=False), so a loop can't carry on past the end of its chunk.
Two cheap filters run after Whisper. One drops a segment that contains a phrase from a short list of things Whisper invents on silence or music, such as subtitle credits and "thanks for watching". The other drops a segment that would be the third identical one in a row.
Why would a speech model make up text?
A common explanation is that Whisper's training data included a lot of subtitled video, much of which opens or ends with a stock line, such as a subtitle credit or "thanks for watching". With silence or music and nothing to transcribe, the model tends to fall back on those lines.
Telling two voices apart
Speaker labels come from sherpa-onnx: a pyannote segmentation model finds where speech happens, a WeSpeaker embedding turns each stretch into a fingerprint of the voice, and the fingerprints are clustered into exactly two groups. It runs in a separate process, so it overlaps with transcription and can be killed on cancel. Afterwards each word gets the speaker whose segment overlaps it most. A single word sandwiched between two words of the other speaker is relabelled, and speakers are numbered by first appearance. If the diarizer fails, the transcript is still saved, with a note at the top saying it has no labels.
What is diarization?
Working out who spoke when, without knowing who anyone is. It gives you stretches of time labelled "voice A" and "voice B", and nothing more. Matching those stretches to the words from the transcription is a separate job, and it is the one the next widget shows.
The window
The window is a small local web page. A server on 127.0.0.1 at a random port serves it, and pywebview shows it in a native window; if that isn't available it opens in the browser instead. Every API request needs a token generated at launch, and a request with a Host header other than 127.0.0.1 or localhost is refused. The window polls for status about every 0.7 seconds. It shows the phase, a progress bar, an estimate of the time left once 4% is done, and the text of each chunk as it finishes. While a job runs the app calls caffeinate so the Mac stays awake. The file picker is an AppleScript dialog.
The text file is only written at the end. If you cancel, nothing is saved.
What it can't do
Only French and English, and only one or two speakers. The two-speaker mode always splits the voices into exactly two groups, so if you ask for two when only one person is talking it will still try to hand out two labels. The speaker model is called wespeaker_en_voxceleb, which points to English training data (VoxCeleb), and how well it separates French voices hasn't been measured.
The filters are blunt. The phrase list is short, so anything Whisper invents that is not on it gets through. And three identical segments in a row can be real speech, in which case the third one is lost.
It needs a Mac with an Apple chip. The first launch needs the internet, and so does the first use of Best. The double-click isn't entirely free of the terminal: if macOS refuses to open the file the first time, it takes a right-click and Open, and if the file arrives without permission to run, fixing that does need a Terminal command, which is the thing I was trying to spare her. Deleting the folder doesn't remove uv, the copy of Python or uv's download cache that setup put outside it.
Unfinished
It works and it has been used, but I haven't timed it on a long recording and haven't measured its error rate. What I want next is a timing run on a real long recording, an error rate against a transcript I know is right, and a check of the two-speaker labels on French audio.
The models and libraries are other people's work: OpenAI's Whisper, Apple's MLX and the mlx-whisper package, sherpa-onnx from k2-fsa, pyannote segmentation 3.0 and WeSpeaker.
- Stack
- Python 3.12 (installed by uv), mlx-whisper, sherpa-onnx, imageio-ffmpeg, numpy, pywebview
- Made for
- My mom, to transcribe her audio recordings
- Platform
- macOS on Apple Silicon
- Status
- Working
- Code
- Not public