Transcription command

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_PATH is 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 in previous/. Standard output is not the transcript.
  • Language. PEPITO_LANGUAGE (BCP-47 tag with region, fr-FR, or auto) and PEPITO_LANGUAGE_CODE (ISO 639-1, fr, or auto) 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_TASK and PEPITO_GUIDELINES_PATH are 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.

  1. brew install whisper.cpp
  2. On a Mac that is online, get a ggml model, for example ggml-large-v3-turbo.bin (the whisper.cpp repository has a models/download-ggml-model.sh script), and put it in ~/Library/Application Support/whisper/. To use another location or file, set WHISPER_MODEL in the environment of the command line (for example WHISPER_MODEL=/path/to/model.bin "/path/to/transcribe-whisper.sh") or edit the script. The name is outside the PEPITO_ namespace, which pepito reserves. A quantized variant such as q5_0 is smaller, which helps with MDM deployment; its accuracy loss has not been measured here.
  3. chmod +x transcribe-whisper.sh
  4. 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.

  1. brew install whisperkit-cli
  2. 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:
    D="$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"
    This creates models/argmaxinc/whisperkit-coreml/, models/argmaxinc/speakerkit-coreml/ and the tokenizer in models/openai/. To use another folder or Whisper model, set WHISPERKIT_MODELS_DIR or WHISPERKIT_MODEL in the environment of the command line; WHISPERKIT_NUM_SPEAKERS sets the number of speakers instead of detecting it. These names are outside the PEPITO_ 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.
  3. chmod +x transcribe-whisperkit-diarize.sh
  4. 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.

ToolLanguageSRT outputNotes
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-nameNeeds Python and ffmpeg. Downloads the model on first use unless you give a local path.
parakeet-mlxNo option documented--output-format srt (default), --output-dirNeeds Python and ffmpeg. Downloads the model on first use (cache directory configurable).
whisperkit-cli transcribe (WhisperKit)--language frIndirect: --report writes <input name>.srt and .json in --report-path; one segment per word with --word-timestampsHomebrew 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): apple or script.
  • transcriptionCommand (string): the command line, run by /bin/sh -c. When it is absent, pepito falls back to the older transcriptionScriptPath (an absolute path), read between single quotes.
  • transcriptionLanguage (string, optional): a BCP-47 tag, or auto.

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.