From a Teams call to a diarized transcript in one drag-and-drop 🎙️

I record a lot of calls. Not because I like listening to myself, but because I never take good notes while talking. The recording always ended up in the same place: I would open the Mistral console, upload the file, wait, copy the text somewhere. It works, but it is four manual steps for something that should be zero. And the output was a wall of text with no idea of who said what.

So I spent an afternoon with Claude Code fixing the whole chain, from the audio routing on the Mac to a little viewer where I can rename the speakers. Here’s how it went.

Step 1: recording both sides of the call

The first problem is not even about AI. When you record with QuickTime while wearing a USB headset, you only get your own voice. The other participants come out of the headset and never reach any input device.

The classic fix on macOS is BlackHole, a virtual audio driver that acts as a loopback. In Audio MIDI Setup, you create two devices:

  1. A Multi-Output Device that duplicates everything the Mac plays into both the headset and BlackHole.
  2. An Aggregate Device that combines the headset microphone and BlackHole into one input.
audio routing diagram
Fig 1. The routing. Remote voices are duplicated into BlackHole, and QuickTime records the aggregate.

Then it’s only a matter of picking the right device in three places:

WhereSetting
macOS sound outputMulti-Output (“Jabra + BlackHole”)
Teams speakerSame multi-output, or “system settings”
QuickTime microphoneAggregate (“Mic + System Audio”)

One gotcha: enable drift correction on BlackHole inside the aggregate device, otherwise long recordings slowly go out of sync. The other one: the volume slider of the Mac is disabled with a multi-output device, so the volume has to be set on the headset itself.

I keep all of this in an Apple Note because I forget it every single time.

Step 2: calling Voxtral instead of the console

Mistral’s speech-to-text model is called Voxtral. The console is just a front-end to the /v1/audio/transcriptions endpoint, which accepts a multipart upload. The whole “app” is therefore a curl:

curl -sS https://api.mistral.ai/v1/audio/transcriptions \
  -H "x-api-key: $KEY" \
  -F "file=@$f" \
  -F "model=voxtral-mini-latest" \
  -F "language=fr" \
  -F "diarize=true" \
  -F "timestamp_granularities=segment"

The two interesting flags are diarize=true and the segment granularity. With them, the response is no longer a flat text but a list of segments, each with a start time, an end time, and a speaker_id:

{
  "type": "transcription_segment",
  "text": "J'imagine, je ne sais pas oĂą tu vas le rajouter...",
  "start": 0.5,
  "end": 6.9,
  "speaker_id": "speaker_1"
}

On a real 8 minutes call, Voxtral returned 211 segments spread over 3 speakers. The third one only had 2 segments, which is the usual false positive you get from an interruption or a cough. Not a big deal, as we’ll see.

Where does the API key live?

I did not want the key in a dotfile or in the script. macOS has a perfectly good secret store, the Keychain, and it is scriptable:

# once
security add-generic-password -a "$USER" -s mistral-api -w 'YOUR_KEY'

# in the script
KEY=$(security find-generic-password -s mistral-api -w)

If the key is missing, the script pops an alert and falls back to opening the console in the browser, so the old workflow is still one drag away.

Step 3: the droplet

I wanted to drag a file onto an icon and be done. AppleScript has had this for twenty years, it’s called a droplet: an app whose on open handler receives the dropped files.

on open theFiles
    set paths to ""
    repeat with f in theFiles
        set paths to paths & " " & quoted form of POSIX path of f
    end repeat
    do shell script "/Users/me/bin/transcribe-mistral.sh" & paths
end open

Compiled with osacompile -o "Transcribe with Mistral.app" droplet.applescript, it sits on the Desktop. Drop one or ten .m4a files on it and a transcript appears next to each one. No Automator, no Shortcuts, no Electron.

Step 4: a viewer where I can rename the speakers

The diarization gives me speaker_1, speaker_2, but it obviously does not know their names. I needed a way to say “speaker_1 is Pierre” once and have it applied everywhere. And to figure out who speaker_1 is, I needed to hear them.

The shell script pipes the JSON into a small Python script that writes a self-contained HTML file next to the audio. No framework, no build, about 60 lines including the CSS.

the transcript viewer
Fig 2. The generated page. Type a name at the top and every turn is renamed. Click a timecode to hear that turn.

A few details I liked:

  • Merging turns. Voxtral returns short segments. Consecutive segments from the same speaker are merged into one turn, so the page reads like a dialogue instead of subtitles.
  • Play from a timecode. The page embeds an <audio> element pointing to the recording in the same folder. Every turn has a â–¶ button that does audio.currentTime = start; audio.play(). While playing, the current turn is highlighted with a timeupdate listener. This is what makes identifying the speakers a ten seconds job.
  • Names survive a reload. They are stored in localStorage, keyed by the file path. The false-positive speaker_3 from earlier simply gets the same name as the real speaker.
  • Export. Two buttons copy the renamed transcript to the clipboard or download it as a .txt, one line per turn with the timestamp.

Alongside the HTML, the script also writes a .md version with a speaker_1 = header to fill in, for the days I’d rather edit in a text editor, and keeps the raw .json in case I want to re-render later.

The whole pipeline

recording.m4a  ──drop──▶  droplet.app
                              │
                              â–Ľ
                     transcribe-mistral.sh    (curl + Keychain)
                              │
                              â–Ľ
                        recording.json
                              │
                              â–Ľ
                     transcript-render.py
                         │          │
                         â–Ľ          â–Ľ
                 recording.html  recording.md

Three files, none of them longer than a screen. The part that took the longest was not the code, it was remembering which audio device goes where.

What I’d change

Voxtral’s diarization is good enough for a two or three people call, but it does not know how many speakers to expect. The API has no num_speakers hint yet, so the occasional ghost speaker will stay. And the HTML viewer expects the audio to stay next to it: move one without the other and the play buttons go silent. Both are fine for my use, which is: record, drop, rename, paste into my notes.

Pierre.

Resources

← Back to projects