feat: add simple audio transcription command

Convert audio and video audio tracks to mono 16 kHz WAV with FFmpeg,
then submit them to OpenRouter's MAI-Transcribe-2 endpoint. Read the API
key from the environment and print the transcript to stdout for easy
redirection. Store request payloads in temporary files to avoid shell
argument size limits, and document dependencies and usage.

Test Plan:
- bash -n transcribe and shellcheck transcribe passed.
- All eight documented formats passed local FFmpeg conversion and mocked
  API request/response checks, including WAV rate and channel validation.
- git diff --cached --check passed.
- No live API request was made.
This commit is contained in:
2026-09-05 11:40:26 +02:00
commit e6b36c2b48
2 changed files with 69 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
# Transcribe
Requires Bash, FFmpeg, curl, jq, and standard coreutils.
```sh
export OPENROUTER_API_KEY='sk-or-v1-...'
./transcribe recording.m4a
./transcribe video.mp4 > transcript.txt
```
Accepts m4a, mp4, aac, flac, opus, mp3, ogg, wav, and other formats your
FFmpeg supports. Uses the first audio track, converts it to mono 16 kHz WAV,
and sends it to OpenRouter's `microsoft/mai-transcribe-2` model. Temporary
files are removed on exit. The transcript goes to stdout; errors to stderr.
One file per invocation; the whole recording is sent in one request, so
the service's audio length and request size limits apply.
[OpenRouter model and API reference](https://openrouter.ai/microsoft/mai-transcribe-2)
Executable
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# == 1 && ($1 == -h || $1 == --help) ]]; then
echo "Usage: ${0##*/} AUDIO_FILE"
echo 'Transcribe audio using OPENROUTER_API_KEY; print the text to stdout.'
exit 0
fi
if [[ $# != 1 ]]; then
echo "Usage: ${0##*/} AUDIO_FILE" >&2
exit 1
fi
if [[ ! -f $1 || ! -r $1 ]]; then
echo "Cannot read audio file: $1" >&2
exit 1
fi
: "${OPENROUTER_API_KEY:?Set OPENROUTER_API_KEY first}"
for tool in ffmpeg curl jq base64 mktemp; do
command -v "$tool" >/dev/null || {
echo "Missing dependency: $tool" >&2
exit 1
}
done
tmp=$(mktemp -d)
trap 'rm -rf -- "$tmp"' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
# Normalize any FFmpeg-readable audio (or the first audio track in a video).
ffmpeg -nostdin -hide_banner -loglevel error -i "$(realpath -- "$1")" \
-map 0:a:0 -ac 1 -ar 16000 -c:a pcm_s16le "$tmp/audio.wav"
# Keep large audio payloads out of shell arguments (which have a size limit).
base64 <"$tmp/audio.wav" | tr -d '\n\r' >"$tmp/audio.b64"
jq -n --rawfile data "$tmp/audio.b64" \
'{model: "microsoft/mai-transcribe-2", input_audio: {data: $data, format: "wav"}}' \
>"$tmp/request.json"
curl --silent --show-error --fail-with-body \
https://openrouter.ai/api/v1/audio/transcriptions \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-H 'Content-Type: application/json' \
--data-binary @"$tmp/request.json" -o "$tmp/response.json" || {
cat "$tmp/response.json" >&2
exit 1
}
jq -r 'if (.text | type) == "string" then .text
else error("Transcription failed: " + (.error.message // tojson)) end' \
"$tmp/response.json"