Skip to content

Custom Transcription Script

This page explains how to transcribe voicemail and recorded calls with your own speech-to-text engine instead of Amazon, Google, or Thirdlane. It describes the command line the platform runs, what it expects back, how to install and select the script, and how to tell from the logs whether it worked.

Why you would use it

The built-in providers send audio to a cloud service. That is the right answer for most installations, but not all:

  • The audio may not leave your network. Health care, legal, government, and some financial deployments have to keep call content on premises. A script that talks to an engine on your own LAN never sends audio anywhere.
  • You already run an engine. If you host Whisper, faster-whisper, Vosk, or a commercial appliance, you have already paid for it and tuned it, and you would rather use it than a second, metered service.
  • You need a language or vocabulary the cloud providers handle poorly. A locally tuned model with your product names and customer names in it can beat a general-purpose service on your own traffic.

The trade-off is that you own the integration: accuracy, uptime, and speed of the engine are yours to manage, and there is no fallback to a cloud provider if the script fails.

How it works

Transcription runs in a background service on the PBX, not inside the call. When a voicemail is left or a recorded call ends:

  1. The platform stages the audio as a local file on the PBX and creates a job, identified by a job key — an opaque string that is unique to that one recording.
  2. The background service runs your script once, passing the path of the staged audio file and the job key on the command line.
  3. Your script transcribes the audio however it likes and prints the transcript to standard output, then exits with status 0.
  4. The platform reads standard output, treats it as the finished transcript, and stores it — in the voicemail email for a voicemail, and against the call record for a recorded call.

The script is run directly by the service, not through a shell, so shell features such as pipes, redirection, and variable expansion in the configured path are not available. Put anything of that kind inside the script itself.

The script runs in the background. The transcription service starts it and carries on accepting and processing other work while it runs, checking once a second whether it has finished, so a script that takes a minute does not delay anything else. Two limits still apply: Maximum concurrent jobs caps how many copies may run at once, and a job that runs for longer than its allotted time is abandoned and its script stopped. See Choosing a concurrency limit and How long a job is given.

The command line your script receives

The platform always invokes the script the same way:

/usr/local/share/thirdlane/service/transcribe/your-script --action transcribe --file /path/to/audio.wav --key 68b4f2c1a3

The three arguments are:

  • --action transcribe — the operation being requested. Today the platform only ever asks for transcribe, meaning “transcribe this file now and print the result”. The argument is passed so that a script which supports other operations can tell them apart.
  • --file <path> — absolute path to the audio file on the PBX. Treat it as read-only: for a recorded call it is usually the recording itself, in the Asterisk spool, not a throwaway copy, so a script that moves, edits or deletes it destroys the recording. Only files the platform copied for the job — a converted recording, or the audio pulled out of a voicemail message — are removed afterwards. Most recordings are 8 kHz or 16 kHz mono WAV; do not assume a fixed sample rate, and convert inside your script if your engine needs something else. Write any working files of your own somewhere else, for example under /tmp.
  • --key <job key> — the platform’s identifier for this job. It is useful for correlating your own logs with the platform’s, and as a name for temporary files. It carries no meaning beyond that.

Arguments may be supplied in any order, so parse them by name rather than by position. Arguments that your script does not recognise should be ignored rather than treated as an error, so that a future platform release which passes an additional flag does not break your integration.

What your script must return

  • Standard output is the transcript, as plain UTF-8 text. Leading and trailing whitespace is trimmed; everything else, including line breaks, is preserved and stored as written.
  • Exit status 0 means success. Any other status marks the job as failed.
  • Standard error is for diagnostics. It is captured and written to the platform log when the script fails, and ignored when it succeeds. Send progress messages and warnings there, never to standard output — anything on standard output becomes part of the transcript.

Printing nothing is treated as a failed transcription, not as an empty one. The voicemail email then says transcription is not available, and no transcript is attached to the recording.

Setting it up

1. Install the script

The script has to live in the transcription service directory, which is the only place the platform will run one from:

/usr/local/share/thirdlane/service/transcribe/

Copy your script there and make it executable:

Terminal window
install -m 0755 whisper-transcribe.sh /usr/local/share/thirdlane/service/transcribe/

The directory is not managed by the platform and is not overwritten by upgrades, but it is also not backed up with the configuration — keep the script in your own version control.

2. Create the Speech Service

In Configuration Manager, open Speech Services and add a service:

  • Purpose — Transcription.
  • Provider — Custom.
  • Description — a name you will recognise when assigning it, for example Whisper (on-prem).
  • Script — pick your script from the dropdown. It lists the executable files in the directory above; the full path is what gets stored.
  • Extra arguments — optional; see below.
  • Maximum concurrent jobs — see Choosing a concurrency limit.
  • Job timeout (minutes) — optional; see How long a job is given.
  • Environment variables — optional; see below.

3. Pass settings to the script

Anything your script needs beyond the audio file — the address of your engine, a model name, an API token, a language — can be supplied in either of two ways.

Environment variables are the better place for secrets, and for anything that is configuration rather than a command-line option. Add them as rows in the Environment variables grid. Each row is one variable, set in the script’s environment for the duration of the run:

VariableValue
WHISPER_URLhttp://127.0.0.1:9000
WHISPER_MODELsmall
WHISPER_LANGUAGEen

Names must start with a letter or underscore and contain only letters, digits, and underscores. Values may contain =, so a URL with a query string or a token with padding is safe to store. Duplicate names are rejected when the service is saved.

Extra arguments suit a script that already takes options, or one you did not write. Put one argument per line; they are inserted before the three standard arguments:

--model
small
--language
en

That runs your-script --model small --language en --action transcribe --file ... --key ....

If you would rather place the file path and job key yourself — because your script expects them somewhere other than the end, or under different option names — use the {file} and {key} placeholders in the extra arguments. When {file} appears anywhere in them, the platform substitutes both placeholders and appends nothing of its own:

--input
{file}
--id
{key}

That runs your-script --input /path/to/audio.wav --id 68b4f2c1a3, with no --action, so a script driven this way has to know what to do without being told.

Choosing a concurrency limit

Maximum concurrent jobs caps how many copies of the script run at the same time; further jobs wait for a free slot. It defaults to 2.

Set it to what your engine can absorb, not to what the PBX can start. A single-GPU Whisper instance generally serves one or two requests at a time and queues or slows down beyond that, so a high limit does not make transcription finish sooner — it makes every job slower at once and increases the chance that jobs run out of time. If the engine runs on the PBX itself, keep the limit low enough that transcription cannot starve call processing.

How long a job is given

A job that never finishes cannot be waited on forever: it would hold its concurrency slot, and the recording it belongs to would never be dealt with either way. Each job therefore has a deadline, and when it passes the script is stopped and the recording is reported as having no transcription available.

By default that deadline is worked out from the length of the recording — five times its duration, plus ten minutes, with anything shorter than the queue check interval (thirty seconds unless it has been changed) counted as thirty seconds — so a long call is given proportionally longer than a short one. A two-minute voicemail gets about twenty minutes; a two-hour call gets about ten hours. That suits an engine that transcribes at roughly the speed the audio plays.

A self-hosted engine often does not work that way. A modest CPU running a large model can spend several minutes on a twenty-second voicemail, which is far longer than a deadline derived from twenty seconds of audio. Job timeout (minutes) exists for that case: set it to the longest a single run of your script should reasonably take, and jobs are given at least that long.

The value is a floor, not a replacement. The platform uses whichever is longer, the derived deadline or the one you set, so raising it can only extend the wait — a two-hour recording still gets the ten hours its length earns it, whatever the field says. Leave the field empty to use the derived deadline alone.

Time a few real recordings through your script before choosing a value, and leave room above the slowest. Remember that jobs also wait for a free slot: at a concurrency limit of 1, a job queued behind two others starts much later than it was created. Waiting does not shorten the deadline — a job waiting for a slot is given the same window as one that is running, including any floor you set.

The field accepts whole minutes up to 1440 (24 hours); a larger number is reduced to that.

4. Assign the service and enable transcription

Recorded calls and voicemail are turned on separately, and each is switched on in two places: once on the tenant, which permits it and chooses the service, and once per user, which turns it on for that person.

For recorded calls, on the tenant:

  1. Set Allow Recorded Calls Transcription to yes.
  2. Choose your custom service in Using Service beside it.
  3. Set Transcribe to Text to yes on each user extension that should get transcripts.

For voicemail, on the tenant:

  1. Set Post-call processing to Messages transcribed to text.
  2. Choose your custom service in the Using Service field that appears.
  3. Set Transcribe to text to yes in the voicemail settings of each user extension or mailbox that should get transcripts.

A user whose tenant permits transcription but who has neither box ticked gets none, which is the way to pilot the service on a few people before enabling it widely.

A minimal working script

This calls a self-hosted whisper-asr-webservice instance and prints what it returns. It is short on purpose — the shape is what matters, not the engine.

#!/bin/bash
set -euo pipefail
FILE=""
while [ $# -gt 0 ]; do
case "$1" in
--file) FILE="$2"; shift 2 ;;
--action) shift 2 ;;
--key) shift 2 ;;
*) shift ;;
esac
done
if [ -z "$FILE" ] || [ ! -f "$FILE" ]; then
echo "no audio file supplied" >&2
exit 1
fi
curl --silent --show-error --fail \
--form "audio_file=@${FILE}" \
"${WHISPER_URL:-http://127.0.0.1:9000}/asr?output=txt&task=transcribe"

Note the two habits worth copying: unrecognised arguments are skipped rather than rejected, and the failure message goes to standard error while only the transcript goes to standard output.

Test it by hand as the root user before wiring it up, using the exact command line the platform will use:

Terminal window
/usr/local/share/thirdlane/service/transcribe/whisper-transcribe.sh \
--action transcribe --file /tmp/sample.wav --key manual-test

If that prints a transcript and nothing else, the script is ready.

Checking that it worked

Transcription activity is logged on the PBX at:

/var/log/thirdlane/tltc.log

Every custom-script run writes the exact command that was executed, prefixed with CMD:, and a line recording how the script exited. Those two are the fastest way to confirm the platform is calling the script the way you expect:

Terminal window
grep -E 'CMD:|transcription script' /var/log/thirdlane/tltc.log | tail

A failed run is logged with the script’s exit code and whatever it wrote to standard error, which is usually enough to identify the cause without reproducing it.

Common outcomes and what they mean:

  • The voicemail email says transcription is not available. The job ran but produced no transcript. Check the log for the CMD: line and the failure that followed it.
  • custom transcription script is not executable. The stored path is missing or has lost its execute bit — most often after the script was replaced by copying a new version over it.
  • transcription script produced no output. The script exited successfully but printed nothing. Run it by hand with the same arguments to see what it does.
  • No CMD: line at all for the recording. The job never reached the provider. Confirm that transcription is allowed on the tenant and enabled on the extension, and that the service is the one assigned.

Best practices

  • Never write anything but the transcript to standard output. Progress lines, warnings, and timing information all end up inside the transcript that users read.
  • Fail loudly. Exit non-zero and explain why on standard error. A script that exits 0 with no output produces a job that silently yields nothing.
  • Match the concurrency limit to the engine, not to the PBX. See Choosing a concurrency limit.
  • Keep credentials in environment variables, not in the script. Values entered on the service are stored with the configuration and are easier to rotate than a file on disk.
  • Test with real recordings, not just clean samples. Voicemail is short, noisy, and often 8 kHz; a model that performs well on studio audio can do poorly on it.
  • Handle being stopped. An abandoned job has its script terminated with SIGTERM, and killed outright if it is still there ten seconds later. If yours writes temporary files, trap that signal and clean them up.