Transcription command
Last updated: 2026-10-30
1. What it is
pepito has two transcription engines. Apple is the default: macOS speech recognition, on your Mac. Command is a command line of your choice, or one your IT department deploys: pepito hands it the audio and the settings of the job, and files the SRT it produces as the transcript. Everything downstream (minutes, search, MCP tools) works the same way.
Privacy: pepito passes the audio and the recording's files to this command, on this Mac. What the command does with them, for example sending them to a server, is up to the command. The statements elsewhere on this site about on-device transcription describe the Apple engine.
2. The contract
Requires pepito 26.61 or later.
The call, the working folder, the variables, the time limit and the queue are common to your three commands (transcription, LLM and post-processing) and described on Commands. What belongs to the transcription command:
-
Input. pepito converts the recording to WAV, PCM 16-bit, mono, 16 kHz in the
working folder and gives its path in
PEPITO_INPUT_PATH. If the conversion fails, the command is not started and the step fails.PEPITO_AUDIO_PATHis the recording's own audio, not that WAV. -
Output. Write an SRT file to
PEPITO_OUTPUT_PATH(in the working folder, ending in.srt) and exit 0. If the file is missing, empty or only whitespace, the step fails. Otherwise pepito stores it as the transcript byte for byte, without parsing or validating it; the previous transcript is kept inprevious/. Standard output is not the transcript. -
Language.
PEPITO_LANGUAGE(BCP-47 tag with region,fr-FR, orauto) andPEPITO_LANGUAGE_CODE(ISO 639-1,fr, orauto) are always set; the command may ignore them. - Failure. A non-zero exit code fails the step, with no fallback to the Apple engine; the error message is the end of standard error.
- Time limit. 10 minutes plus a quarter of the duration of the converted WAV (25 minutes for a one-hour recording), counted from the moment the command starts.
-
Test. The Test button of Settings has macOS read a short sentence in your default transcription
language and passes it to the command, with no recording: only the variables above,
PEPITO_TASKandPEPITO_GUIDELINES_PATHare set.
3. Example: whisper.cpp
Download transcribe-whisper.sh.
It relies on whisper-cli from whisper.cpp
and a local ggml model. It never downloads anything.
#!/bin/bash
# pepito transcription command, example based on whisper.cpp (whisper-cli).
# Requires pepito 26.61 or later.
#
# Set the path of this file as the transcription command in pepito (or a
# command line that runs it). pepito passes the job in environment
# variables (PEPITO_INPUT_PATH, PEPITO_OUTPUT_PATH, ...).
# It must write an SRT file to PEPITO_OUTPUT_PATH and exit 0.
# Any non-zero exit code is reported by pepito as a failed transcription,
# with the end of stderr as the message.
#
# This script never downloads anything: install whisper.cpp and copy the
# model beforehand (see the page that publishes this file).
#
# Not used here: PEPITO_TASK and the other variables of the recording
# (absent when Settings runs the Test button).
set -euo pipefail
# Model file. WHISPER_MODEL is specific to this script (outside the PEPITO_
# namespace, which pepito reserves and clears); pepito ignores it.
MODEL="${WHISPER_MODEL:-$HOME/Library/Application Support/whisper/ggml-large-v3-turbo.bin}"
# Preflight checks.
if ! command -v whisper-cli >/dev/null 2>&1; then
echo "whisper-cli not found in PATH (brew install whisper.cpp)" >&2
exit 1
fi
if [ ! -f "$MODEL" ]; then
echo "model not found: $MODEL" >&2
exit 1
fi
case "${PEPITO_OUTPUT_PATH:-}" in
*.srt) ;;
*)
echo "PEPITO_OUTPUT_PATH must end with .srt: ${PEPITO_OUTPUT_PATH:-<unset>}" >&2
exit 1
;;
esac
# Language: whisper-cli defaults to English, so always pass it.
# PEPITO_LANGUAGE_CODE is an ISO 639-1 code ("fr") or "auto", which whisper-cli
# understands as is.
#
# -osrt -of <base>: whisper-cli appends ".srt" to the base name.
# -np: no progress or diagnostic output; whisper-cli still prints the
# segments on stdout, hence >/dev/null (pepito logs stdout privately, but
# there is no reason to print the transcript). Errors stay on stderr.
whisper-cli -m "$MODEL" -f "$PEPITO_INPUT_PATH" -l "$PEPITO_LANGUAGE_CODE" \
-osrt -of "${PEPITO_OUTPUT_PATH%.srt}" -np >/dev/null
Install
Requires pepito 26.61 or later: the script reads PEPITO_INPUT_PATH, which older versions do not set.
brew install whisper.cpp- On a Mac that is online, get a ggml model, for example
ggml-large-v3-turbo.bin(the whisper.cpp repository has amodels/download-ggml-model.shscript), and put it in~/Library/Application Support/whisper/. To use another location or file, setWHISPER_MODELin the environment of the command line (for exampleWHISPER_MODEL=/path/to/model.bin "/path/to/transcribe-whisper.sh") or edit the script. The name is outside thePEPITO_namespace, which pepito reserves. A quantized variant such asq5_0is smaller, which helps with MDM deployment; its accuracy loss has not been measured here. chmod +x transcribe-whisper.sh- In pepito, open Settings, Transcription, choose the Command engine, type the command line or click Choose… to insert the path of the file, then click Test. Test has macOS read a short sentence in your default transcription language and passes it to the command.
The script passes -l "$PEPITO_LANGUAGE_CODE", because whisper-cli defaults to English;
auto is understood as is.
4. Example: WhisperKit with speaker labels
Download transcribe-whisperkit-diarize.sh.
It relies on whisperkit-cli from WhisperKit,
on a Mac with Apple silicon, and on local models: a Whisper model for the transcript and the SpeakerKit models for
speaker diarization. It never downloads anything.
Each segment starts with a speaker label: [A], [B], and so on. Labels are letters
within one recording, not names, and the same letter may be another person in the next recording. With a single
speaker, segments have no label.
#!/bin/bash
# pepito transcription command, example based on WhisperKit (whisperkit-cli),
# with speaker labels ([A], [B], ...) from its speaker diarization.
# Requires pepito 26.61 or later, and a Mac with Apple silicon.
#
# Set the path of this file as the transcription command in pepito (or a
# command line that runs it). pepito passes the job in environment
# variables (PEPITO_INPUT_PATH, PEPITO_OUTPUT_PATH, ...).
# It must write an SRT file to PEPITO_OUTPUT_PATH and exit 0.
# Any non-zero exit code is reported by pepito as a failed transcription,
# with the end of stderr as the message.
#
# This script never downloads anything: install whisperkit-cli and download
# the models beforehand (see the page that publishes this file).
#
# Not used here: PEPITO_TASK and the other variables of the recording
# (absent when Settings runs the Test button).
set -euo pipefail
# Settings specific to this script (outside the PEPITO_ namespace, which
# pepito reserves and clears); pepito ignores them.
MODEL="${WHISPERKIT_MODEL:-large-v3-v20240930_turbo_632MB}"
MODELS_DIR="${WHISPERKIT_MODELS_DIR:-$HOME/Library/Application Support/whisperkit-models}"
NUM_SPEAKERS="${WHISPERKIT_NUM_SPEAKERS:-}"
WHISPER_MODEL_PATH="$MODELS_DIR/models/argmaxinc/whisperkit-coreml/openai_whisper-$MODEL"
DIARIZATION_MODEL_PATH="$MODELS_DIR/models/argmaxinc/speakerkit-coreml"
# Preflight checks. whisperkit-cli downloads whatever is missing, the
# tokenizer included, so the script refuses to start without them.
if ! command -v whisperkit-cli >/dev/null 2>&1; then
echo "whisperkit-cli not found in PATH (brew install whisperkit-cli)" >&2
exit 1
fi
if [ ! -d "$WHISPER_MODEL_PATH" ]; then
echo "model not found: $WHISPER_MODEL_PATH" >&2
exit 1
fi
if [ ! -d "$DIARIZATION_MODEL_PATH" ]; then
echo "diarization models not found: $DIARIZATION_MODEL_PATH" >&2
exit 1
fi
if ! ls "$MODELS_DIR"/models/openai/*/tokenizer.json >/dev/null 2>&1; then
echo "tokenizer not found in: $MODELS_DIR/models/openai" >&2
exit 1
fi
case "${PEPITO_OUTPUT_PATH:-}" in
*.srt) ;;
*)
echo "PEPITO_OUTPUT_PATH must end with .srt: ${PEPITO_OUTPUT_PATH:-<unset>}" >&2
exit 1
;;
esac
# The current directory is pepito's working folder, deleted at the end.
# whisperkit-cli prints its progress and the transcript on stdout, hence
# >/dev/null (pepito logs stdout privately, but there is no reason to print
# the transcript). Errors stay on stderr.
# 1. Speaker turns, as RTTM.
diarize=(whisperkit-cli diarize --audio-path "$PEPITO_INPUT_PATH"
--rttm-path "$PWD/diarization.rttm" --model-path "$DIARIZATION_MODEL_PATH")
if [ -n "$NUM_SPEAKERS" ]; then
diarize+=(--num-speakers "$NUM_SPEAKERS")
fi
"${diarize[@]}" >/dev/null
# 2. Transcript with word timestamps: --report writes an SRT with one
# segment per word in --report-path.
# Language: PEPITO_LANGUAGE_CODE is an ISO 639-1 code ("fr") or "auto";
# without --language, Whisper detects the language.
transcribe=(whisperkit-cli transcribe --audio-path "$PEPITO_INPUT_PATH"
--model-path "$WHISPER_MODEL_PATH" --download-tokenizer-path "$MODELS_DIR"
--word-timestamps --skip-special-tokens --report --report-path "$PWD/report")
if [ "$PEPITO_LANGUAGE_CODE" != "auto" ]; then
transcribe+=(--language "$PEPITO_LANGUAGE_CODE")
fi
mkdir -p "$PWD/report"
"${transcribe[@]}" >/dev/null
words=("$PWD"/report/*.srt)
if [ ! -f "${words[0]}" ]; then
echo "whisperkit-cli wrote no SRT in $PWD/report" >&2
exit 1
fi
# 3. Each word goes to the speaker whose turn overlaps it most (the nearest
# turn if none does). A new segment starts on a change of speaker, after a
# sentence end (. ? !) or after a silence of more than 1 s. A token without
# a leading space continues the previous word: same speaker, no cut.
# Labels are added only when more than one speaker is found.
LC_ALL=C awk -v GAP=1.0 '
function secs(t, p) { split(t, p, /[:,]/); return p[1] * 3600 + p[2] * 60 + p[3] + p[4] / 1000 }
function stamp(t, ms) {
ms = int(t * 1000 + 0.5)
return sprintf("%02d:%02d:%02d,%03d", int(ms / 3600000), int(ms / 60000) % 60, int(ms / 1000) % 60, ms % 1000)
}
function speaker(s, e, i, o, d, best, bo, bd, mid) {
bo = 0; best = ""
for (i = 1; i <= nt; i++) {
o = (e < te[i] ? e : te[i]) - (s > ts[i] ? s : ts[i])
if (o > bo) { bo = o; best = tk[i] }
}
if (best != "" || nt == 0) return best
mid = (s + e) / 2; bd = -1
for (i = 1; i <= nt; i++) {
d = mid - ts[i]; if (d < 0) d = -d
o = mid - te[i]; if (o < 0) o = -o
if (o < d) d = o
if (bd < 0 || d < bd) { bd = d; best = tk[i] }
}
return best
}
function word(s, e, t, bare, cont, k) {
bare = t; gsub(/^[ \t]+|[ \t]+$/, "", bare)
if (bare == "" || bare == "...") return
cont = n > 0 && substr(t, 1, 1) != " "
k = cont ? cs[n] : speaker(s, e)
if (n == 0 || (!cont && (k != cs[n] || stop || s - ce[n] > GAP))) {
n++; cb[n] = s; cs[n] = k; ct[n] = ""
if (k != "") seen[k] = 1
}
ce[n] = e; ct[n] = ct[n] t
stop = bare ~ /[.?!]["»”)]*$/
}
FNR == NR {
if ($1 == "SPEAKER") { nt++; ts[nt] = $4 + 0; te[nt] = $4 + $5; tk[nt] = $8 }
next
}
/-->/ { s = secs($1); e = secs($3); text = ""; intext = 1; next }
intext && /^$/ { word(s, e, text); intext = 0; next }
intext { text = text $0; next }
END {
if (intext) word(s, e, text)
for (k in seen) ns++
for (i = 1; i <= n; i++) {
t = ct[i]; gsub(/^[ \t]+|[ \t]+$/, "", t)
if (t == "") continue
if (ns > 1 && cs[i] != "") t = "[" cs[i] "] " t
printf "%d\n%s --> %s\n%s\n\n", ++m, stamp(cb[i]), stamp(ce[i]), t
}
}
' "$PWD/diarization.rttm" "${words[0]}" > "$PEPITO_OUTPUT_PATH"
Install
Requires pepito 26.61 or later: the script reads PEPITO_INPUT_PATH, which older versions do not set.
brew install whisperkit-cli- On a Mac that is online, download the models once (about 630 MB) into
~/Library/Application Support/whisperkit-models/, by running both tools on a short sample:
This createsD="$HOME/Library/Application Support/whisperkit-models" say -o /tmp/sample.aiff "Hello" && afconvert -f WAVE -d LEI16@16000 -c 1 /tmp/sample.aiff /tmp/sample.wav whisperkit-cli diarize --audio-path /tmp/sample.wav --download-model-path "$D" whisperkit-cli transcribe --audio-path /tmp/sample.wav --model large-v3-v20240930_turbo_632MB --download-model-path "$D"models/argmaxinc/whisperkit-coreml/,models/argmaxinc/speakerkit-coreml/and the tokenizer inmodels/openai/. To use another folder or Whisper model, setWHISPERKIT_MODELS_DIRorWHISPERKIT_MODELin the environment of the command line;WHISPERKIT_NUM_SPEAKERSsets the number of speakers instead of detecting it. These names are outside thePEPITO_namespace, which pepito reserves. The SpeakerKit models are published under CC BY 4.0 (see their model card); if you redistribute them, for example in an internal package, keep the attribution. chmod +x transcribe-whisperkit-diarize.sh- In pepito, open Settings, Transcription, choose the Command engine, type the command line or click Choose… to insert the path of the file, then click Test. The test sentence has one voice, so its segment has no label. The first run after installing is slower while macOS prepares the models.
Each word goes to the speaker whose turn overlaps it most. A new segment starts on a change of speaker, after
a sentence end or after a pause of more than one second. Limits seen in testing: a short reply ("yes", "ok")
inside a long turn may go to the main speaker, overlapping speech can be split between two labels, and a passage
that Whisper leaves without punctuation becomes one long segment. Summaries see the labels as written; you can
say what they mean in GUIDELINES.md.
5. Preparing a Mac without network access
With whisper.cpp, everything is local once two things are in place: the whisper-cli binary and
the model file. Copy the model to the path the script expects (or deploy it), on the Mac that will not be online.
Nothing is downloaded at run time.
With the WhisperKit example, the same holds once the whisperkit-cli binary and the models folder
are in place: copy ~/Library/Application Support/whisperkit-models/ from a Mac where the install
step ran. The script checks that the models and the tokenizer are there before it starts, because
whisperkit-cli downloads whatever is missing, even with a local model path.
When Command is the engine, pepito does not need Apple's servers for transcription; installing the macOS transcription model is only needed for the Apple engine, for instance if someone reruns a transcription with it. Whether the tool you choose reaches the network is a property of that tool. Summaries are separate: they need a network only if you use a remote model.
6. Other tools
Options that map to what the contract provides, from a reading of each tool's documentation and source
(2026-10-13). Nothing was installed or run, except whisperkit-cli 1.1.1 for the example above
(2026-10-30); items marked "not documented" were not found in the sources read.
A wrapper script turns the variables into each tool's options.
| Tool | Language | SRT output | Notes |
|---|---|---|---|
whisper-cli (whisper.cpp) | -l fr or -l auto; default is en | -osrt -of <base> (adds .srt) | Local ggml model file. Used by the example above. |
mlx_whisper (mlx-whisper) | --language fr; detection when absent | --output-format srt, with --output-dir and --output-name | Needs Python and ffmpeg. Downloads the model on first use unless you give a local path. |
parakeet-mlx | No option documented | --output-format srt (default), --output-dir | Needs Python and ffmpeg. Downloads the model on first use (cache directory configurable). |
whisperkit-cli transcribe (WhisperKit) | --language fr | Indirect: --report writes <input name>.srt and .json in --report-path; one segment per word with --word-timestamps | Homebrew formula whisperkit-cli. --model-path for a local model. --diarization prints speaker turns in its console output only, not in the report; the example above uses whisperkit-cli diarize --rttm-path instead. |
Two tools were considered and set aside:
- yap uses the same macOS speech recognition as the Apple engine, so it adds nothing here.
- FluidAudio (
fluidaudio transcribe) has no SRT output, only JSON and text. A wrapper that builds the SRT would be needed.
7. Deployment with MDM
Preference domain: fr.sodadi.pepito. Keys:
transcriptionEngine(string):appleorscript.transcriptionCommand(string): the command line, run by/bin/sh -c. When it is absent, pepito falls back to the oldertranscriptionScriptPath(an absolute path), read between single quotes.transcriptionLanguage(string, optional): a BCP-47 tag, orauto.
Your IT department deploys the script and its model itself (package, executable bit, absolute path in the command line), and the tool the script calls. Example of a configuration profile payload, as an Application Custom Settings payload; adapt identifiers and UUIDs:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadType</key><string>Configuration</string>
<key>PayloadVersion</key><integer>1</integer>
<key>PayloadIdentifier</key><string>com.example.pepito.transcription</string>
<key>PayloadUUID</key><string>11111111-1111-1111-1111-111111111111</string>
<key>PayloadDisplayName</key><string>pepito transcription command</string>
<key>PayloadContent</key>
<array>
<dict>
<key>PayloadType</key><string>fr.sodadi.pepito</string>
<key>PayloadVersion</key><integer>1</integer>
<key>PayloadIdentifier</key><string>com.example.pepito.transcription.settings</string>
<key>PayloadUUID</key><string>22222222-2222-2222-2222-222222222222</string>
<key>transcriptionEngine</key><string>script</string>
<key>transcriptionCommand</key><string>"/usr/local/share/pepito/transcribe-whisper.sh"</string>
<key>transcriptionLanguage</key><string>auto</string>
</dict>
</array>
</dict>
</plist>
The value is not locked. The profile sets a default, but a user can change the engine or the command in Settings, and rerun a recording with another engine. If you need to guarantee that audio goes only to your command, control it outside pepito as well.