transcribe-proxy

September 8, 2026 · View on GitHub

Run a proxy server that forwards transcription requests to your configured ASR provider (Wyoming, OpenAI, or Gemini).

This is useful for integrating agent-cli's transcription capabilities into other applications, such as iOS Shortcuts.

Usage

agent-cli server transcribe-proxy [OPTIONS]

Examples

# Run on default port
agent-cli server transcribe-proxy

# Custom port
agent-cli server transcribe-proxy --port 8080

# Development mode with auto-reload
agent-cli server transcribe-proxy --reload

Options

Options

OptionDefaultDescription
--host0.0.0.0Network interface to bind. Use 0.0.0.0 for all interfaces
--port, -p61337Port for the HTTP API
--reloadfalseAuto-reload on code changes (development only)

General Options

OptionDefaultDescription
--log-levelinfoSet logging level.

API Endpoints

EndpointMethodDescription
/transcribePOSTTranscribe audio file, optionally with speaker labels
/diarizePOSTIdentify speaker timestamps without transcription
/healthGETHealth check
/docsGETInteractive API documentation

POST /transcribe

Transcribe an audio file with optional LLM post-processing.

Request Parameters (multipart/form-data):

ParameterTypeDefaultDescription
audiofilerequiredAudio file (wav, mp3, m4a, ogg, flac, aac, webm)
cleanupbooleantrueWhether to apply LLM post-processing to clean up the transcript
diarizebooleanfalseReturn speaker-labeled segments; requires cleanup=false
min_speakersinteger-Optional minimum number of speakers, at least 1
max_speakersinteger-Optional maximum number of speakers, at least the minimum
align_wordsbooleanfalseUse forced word alignment; requires diarize=true
align_languagestringenForced alignment language: en, fr, de, es, it
extra_instructionsstring-Additional instructions for the LLM cleanup (appended to any config file instructions)

Disabling LLM Post-Processing:

If you only need raw transcription without LLM cleanup (e.g., for simple note-taking or environments without LLM infrastructure), pass cleanup=false:

curl -X POST http://localhost:61337/transcribe \
  -F "audio=@recording.wav" \
  -F "cleanup=false"

This skips the LLM step entirely, reducing latency and removing the LLM dependency for that request.

Response:

{
  "raw_transcript": "the original transcription",
  "cleaned_transcript": "The cleaned transcription.",
  "success": true,
  "error": null,
  "segments": null
}
FieldTypeDescription
raw_transcriptstringThe raw transcription from the ASR provider
cleaned_transcriptstring or nullThe LLM-cleaned transcript, or null if cleanup=false
successbooleanWhether the transcription succeeded
errorstring or nullError message if something went wrong
segmentsarray or nullSpeaker segments (speaker, start, end, text), or null without diarization

Speaker Diarization

Install the optional dependencies and set HF_TOKEN on the server:

uv sync --extra server --extra wyoming --extra llm --extra diarization
export HF_TOKEN=your-huggingface-token
agent-cli server transcribe-proxy

Accept the model access conditions for speaker-diarization-3.1, segmentation-3.0, and wespeaker-voxceleb-resnet34-LM. Use a token with read access to these models. The first request downloads the models; later requests reuse them.

For a speaker-labeled transcript, configure your usual ASR backend and send:

curl http://localhost:61337/transcribe \
  -F "audio=@interview.wav" \
  -F "cleanup=false" -F "diarize=true" \
  -F "min_speakers=2" -F "max_speakers=2" \
  -F "align_words=true" -F "align_language=en"

raw_transcript contains the original ASR text. The additional segments field contains:

[
  {"speaker": "SPEAKER_00", "start": 0.4, "end": 2.1, "text": "Hello there."},
  {"speaker": "SPEAKER_01", "start": 2.3, "end": 4.0, "text": "Welcome back."}
]

align_words=true uses the existing wav2vec2 alignment and downloads an additional language model. Without it, sentence timing is estimated from transcript length, so speaker assignment is approximate. Word alignment also falls back to this estimate if it cannot align any words. Speaker labels are local to each upload; this API does not enroll or identify persistent voice profiles.

POST /diarize

Return speaker labels and timestamps without using an ASR provider:

curl http://localhost:61337/diarize \
  -F "audio=@interview.wav" \
  -F "min_speakers=2" -F "max_speakers=2"

Accepts the same audio upload and optional min_speakers / max_speakers hints as /transcribe. Returns {"segments": [{"speaker": "SPEAKER_00", "start": 0.4, "end": 2.1, "text": ""}]}. Times are seconds from the start of the file. Overlapping speech can produce overlapping segments; silence returns an empty list.

Both endpoints delete temporary audio after processing. The model loads lazily and remains in memory until the process exits; concurrent diarization requests run sequentially while health checks remain responsive. Use one server worker to avoid loading extra model copies. /health checks the HTTP service; it does not verify model access or readiness. Missing token or dependencies return HTTP 503, invalid options return 422, empty uploads return 400, and inference failures return 500.

How It Works

The transcription proxy acts as a bridge between client applications and your configured ASR provider:

┌─────────────────┐     ┌─────────────────────┐     ┌─────────────────┐
│  iOS Shortcut   │────▶│  Transcription      │────▶│  ASR Provider   │
│  or other app   │     │  Proxy (:61337)     │     │  (configured)   │
└─────────────────┘     └─────────────────────┘     └─────────────────┘

                              ┌────────────────────────────┼────────────────────────────┐
                              ▼                            ▼                            ▼
                        ┌──────────┐                 ┌──────────┐                 ┌──────────┐
                        │ Wyoming  │                 │  OpenAI  │                 │  Gemini  │
                        │ (Local)  │                 │  (Cloud) │                 │  (Cloud) │
                        └──────────┘                 └──────────┘                 └──────────┘

The proxy reads your agent-cli configuration to determine which ASR provider to use, then forwards transcription requests accordingly.

Use Cases

iOS Shortcuts Integration

The proxy provides a simple HTTP endpoint that iOS Shortcuts can call to transcribe audio. See the iOS Shortcut Guide for setup instructions.

Custom Applications

Any application that can make HTTP requests can use the proxy to access transcription services without needing to implement provider-specific logic.

Installation

Requires the server extra:

pip install "agent-cli[server]"
# or
uv sync --extra server

Docker

Run using the published container image:

# Run standalone
docker run -p 61337:61337 ghcr.io/basnijholt/agent-cli-transcribe-proxy:latest

# Or use docker-compose (included in both cuda and cpu profiles)
docker compose -f docker/docker-compose.yml --profile cpu up transcribe-proxy

Docker with diarization

Run the published image, which includes pyannote, PyTorch, and FFmpeg:

docker run --rm -p 61337:61337 \
  -e HF_TOKEN \
  -v agent-cli-diarization-cache:/home/transcribe/.cache \
  ghcr.io/basnijholt/agent-cli-diarization:latest

This runs /diarize on CPU. For NVIDIA GPU inference, add --gpus all -e DIARIZATION_DEVICE=cuda to docker run. For /transcribe, also provide your ASR environment variables, for example -e ASR_PROVIDER=openai -e ASR_OPENAI_BASE_URL=http://your-whisper-host:10301/v1 -e OPENAI_API_KEY=dummy.

The optional Compose service can run beside the existing Whisper server:

# HF_TOKEN must be exported in the shell or set in docker/.env.
docker compose -f docker/docker-compose.yml --profile cpu up whisper-cpu diarization

Use diarization in place of transcribe-proxy, since both default to port 61337. Set DIARIZATION_PORT to use a different host port. The model cache persists in agent-cli-diarization-cache. GPU instructions are included beside the service in the Compose file.

To build from source instead, add --build to the Compose command or run:

docker build -f docker/diarization.Dockerfile -t agent-cli-diarization .

Environment Variables

Configure the proxy using environment variables (priority: env var > config file > default):

VariableDefaultDescription
HF_TOKEN-Hugging Face read token for diarization models
DIARIZATION_DEVICEautoPyTorch device, e.g. cpu, cuda, cuda:0, mps
ASR_PROVIDERwyomingASR provider: wyoming, openai, gemini
ASR_WYOMING_IPlocalhostWyoming ASR server hostname/IP
ASR_WYOMING_PORT10300Wyoming ASR server port
ASR_OPENAI_MODELwhisper-1OpenAI ASR model name
ASR_OPENAI_BASE_URL-Custom OpenAI-compatible ASR endpoint
ASR_OPENAI_PROMPT-Custom prompt to guide transcription
ASR_GEMINI_MODELgemini-3-flash-previewGemini ASR model name
LLM_PROVIDERollamaLLM provider: ollama, openai, gemini
LLM_OLLAMA_MODELgemma3:4bOllama model name
LLM_OLLAMA_HOSThttp://localhost:11434Ollama server URL
LLM_OPENAI_MODELgpt-5-miniOpenAI model name
LLM_GEMINI_MODELgemini-3-flash-previewGemini model name
TTS_PROVIDERwyomingTTS provider: wyoming, openai, kokoro, gemini
LOG_LEVELinfoLogging level: debug, info, warning, error
OPENAI_API_KEY-OpenAI API key
OPENAI_BASE_URL-Custom OpenAI-compatible API base URL
GEMINI_API_KEY-Gemini API key

Example with Wyoming ASR in Docker Compose:

docker run -p 61337:61337 \
  -e ASR_WYOMING_IP=agent-cli-whisper \
  -e ASR_WYOMING_PORT=10300 \
  ghcr.io/basnijholt/agent-cli-transcribe-proxy:latest

Custom Config File

To use a custom config, mount it as a volume:

docker run -p 61337:61337 \
  -v ./config.toml:/home/transcribe/.config/agent-cli/config.toml:ro \
  ghcr.io/basnijholt/agent-cli-transcribe-proxy:latest