Speech to Text (STT) for FusionPBX. Convert call recordings, voicemails, and other audio into text using a pluggable set of transcription engines (OpenAI, Inworld, local Whisper, Parakeet, Azure, Google, Watson).
- Pluggable engines — pick a provider in the settings and the code loads
transcribe_<engine>automatically. - Speaker diarization for multi-channel recordings.
- Automatic audio segmentation for long recordings (15-minute chunks).
- Two integration modes: synchronous (inline) transcription, and an asynchronous transcription queue processed by a background service.
- A consistent JSON output (
{"segments":[...]}) regardless of engine.
- ffmpeg (required) — used to detect the number of audio channels, measure duration, split long recordings, split channels, and convert audio (for example to 16 kHz mono PCM for Inworld).
- Debian/Ubuntu:
apt install ffmpeg
- Debian/Ubuntu:
- PHP with the cURL and OpenSSL extensions enabled.
- For the queue: a systemd (or similar) service and write access to
/var/run/fusionpbx. - For the
localengine: a self-hosted, OpenAI-compatible Whisper HTTP server.
apt install ffmpeg
cd /var/www/fusionpbx/app
git clone https://github.com/fusionpbx/fusionpbx-app-transcribe.git transcribe
chown -R www-data:www-data /var/www/fusionpbx
php /var/www/fusionpbx/core/upgrade/upgrade.phpThe upgrade step creates the v_transcribe_queue table and registers the default settings, permissions, and menu items.
Open Menu → Default Settings and select the transcribe category (or set values per-domain under Settings).
| Setting | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Master switch for speech-to-text. |
engine |
text | openai |
Provider to use: openai, inworld, local, parakeet, azure, google, watson. |
api_key |
text | — | API key / credential for the provider. |
api_url |
text | — | Provider endpoint. See the provider notes below for the expected value. (optional) |
api_model |
text | engine-specific | Model to request. (optional; for example gpt-transcribe for OpenAI, parakeet-tdt-0.6b-v3 for Parakeet.) |
limit |
numeric | 3 |
Maximum number of queue jobs to run concurrently. |
interval |
numeric | 1 |
Seconds between queue polls. |
save_response |
boolean | false |
Store the raw transcription response in the queue row (useful for debugging). |
Additional settings read by specific engines (add them under the transcribe category if you need them; the code falls back to the shown defaults):
| Setting | Default | Used by | Description |
|---|---|---|---|
diarize |
false |
openai, inworld | Request speaker diarization for multi-channel audio. |
timestamps |
true |
parakeet | Request word-level timestamps in the response (each segment then carries a words array with per-word start/end). |
sample_rate |
16000 |
inworld, parakeet | Sample rate (Hz) to resample audio to before upload. |
stale_timeout |
3600 |
queue service | Seconds before a stuck processing job is reset to pending. |
Note: The settings loader resolves one row per subcategory. If a subcategory (for example
api_key) has more than one enabled row, the last one loaded can win. Avoid duplicate rows, and remember that PHP-FPM with APCu may serve cached settings briefly after a database-only change.
- The
transcribewrapper readsengineand instantiatestranscribe_<engine>(see the provider classes inresources/classes/). - Audio is inspected with
ffprobe(channel count + duration). Recordings shorter than ~2 seconds are skipped. - Long audio is split into ~15-minute (900 s) segments before the API call.
- Multi-channel audio is handled engine-specifically — either by splitting each channel and transcribing it separately, or by requesting diarization (see the provider notes).
transcribe($output_type)returns eithertext(plain transcript) orjson(segmented).transcribe::conversation_format()renders a segmented transcript as an HTML/text "conversation" grouped by speaker.
All engines normalize to the same segment structure:
{
"segments": [
{ "channel": "0", "speaker": "0", "start": 0.0, "end": 3.2, "text": "Hello, how can I help you?" },
{ "channel": "1", "speaker": "1", "start": 3.4, "end": 7.9, "text": "Hi, I'd like to check my balance." }
]
}Other apps call the transcription synchronously when a recording is saved (recordings, voicemails, voicemail greetings). Example:
$transcribe = new transcribe($settings);
$transcribe->audio_path = $recording_path;
$transcribe->audio_filename = $recording_filename;
$message = $transcribe->transcribe('text'); // or 'json'This requires transcribe.enabled = true and a valid engine.
Heavy or call-recording transcription is queued and processed by a background service so the web request returns quickly.
- Jobs are inserted into
v_transcribe_queuewith statuspending(for example by thecall_recordingsandxml_cdrapps, or manually via Menu → Transcribe Queue). - The
transcribe_queueservice polls the queue, claims up tolimitjobs per cycle (usingFOR UPDATE SKIP LOCKED), and spawnsresources/jobs/process.phpfor each. process.phpruns the transcription and updates the row tocompleted/failed, then hands the result to the target class/method named in the row.
Debian/Ubuntu (systemd):
cp /var/www/fusionpbx/app/transcribe/resources/service/debian/transcribe_queue.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now transcribe_queue.service
systemctl status transcribe_queue.serviceA FreeBSD transcribe_queue.service is also provided.
Set transcribe.engine to one of the values below. Each provider class lives in app/transcribe/resources/classes/.
- Default URL:
https://api.openai.com/v1/audio/transcriptions - Auth:
Authorization: Bearer <api_key>, audio uploaded asmultipart/form-data. - Models:
whisper-1,gpt-4o-transcribe,gpt-4o-mini-transcribe,gpt-transcribe(default),gpt-transcribe-1. - Behavior:
- Single-channel →
verbose_jsonfor whisper models; gpt models return plain text (noresponse_format). - Multi-channel +
diarizeon → the model is forced towhisper-1and the request usesresponse_format=diarized_jsonwithchunking_strategy=auto, so you get speaker labels in one pass. - Multi-channel +
diarizeoff → each channel is split out with ffmpeg and transcribed separately.
- Single-channel →
- Settings:
api_key,api_url,api_model,diarize.
- Default URL:
https://api.inworld.ai/stt/v1/transcribe - Auth:
Authorization: Basic <api_key>, JSON request body. - Model:
inworld/inworld-stt-1(the only model). - Audio: Inworld requires 16 kHz mono LINEAR16 (PCM s16le) audio, so each segment is converted with ffmpeg (
-ac 1 -ar <sample_rate> -acodec pcm_s16le) before upload. The audio is base64-encoded into theaudioData.contentfield. - Behavior: single-channel audio is transcribed directly; multi-channel recordings are transcribed automatically with
enableSpeakerDiarization. Word timestamps are grouped into segments on a speaker change or a pause of over ~500 ms. - Settings:
api_key,api_url,api_model,diarize,sample_rate.
- Default URL:
http://127.0.0.1:8000/transcribe - Auth:
Authorization: Bearer <api_key>(only set a key if your server enforces one). - Default model:
whisper-1. - Points at any local, OpenAI-compatible Whisper HTTP server (for example a
faster-whisper,whisper.cpp, orwhisperXHTTP wrapper). Audio is uploaded asmultipart/form-data;response_formatistextfor mono andjsonfor multi-channel. - Settings:
api_key,api_url,api_model.
- Default URL:
http://127.0.0.1:8000/transcribe - Auth: none by default (set
api_keyand it is sent asAuthorization: Bearer). - Default model:
parakeet-tdt-0.6b-v3— multilingual (25 European languages), automatically detects the language of the audio, and adds punctuation and capitalization.parakeet-unified-en-0.6b(English only) is also available as a fallback/comparison model. - Points at a self-hosted Parakeet (NVIDIA NeMo) service, for example the Flask app in the
parakeetproject that servesmodel.transcribe()overPOST /transcribe(multipart/form-datawith afilefield). The service loads both models, selects one with themodelform field (the v3 model is the default), rejects unknown models, andGET /modelslists the available models. - Languages: the v3 model covers 25 European languages (
bg cs da de el en es et fi fr hr hu it lt lv mt nl pl pt ro ru sk sl sv uk) and detects the language automatically, so no per-recording language selection is needed.language(ISO 639-1 code) can still be set as an optional hint — it is sent to the service, the model decides from the audio. CJK and other non-European languages are not covered by this checkpoint. - Audio: the Parakeet/NeMo model only accepts 16 kHz mono PCM WAV (an 8 kHz stereo telephony mp3 is rejected with an input-shape error), so every segment is converted with ffmpeg (
-ac 1 -ar <sample_rate> -acodec pcm_s16le) before upload. - Word-level timestamps: with
timestampsenabled the service is asked for word timing (timestamps=trueform field) and responds with{"text": "...", "words": [{"word","start","end"}]}. The words are grouped into the standard segments (new segment on a pause over ~1 s or at the end of a sentence), so every segment — single-channel voicemails as well as each channel of a call recording — carries awordsarray with per-word start/end times in addition to the usualstart/end/text/speakerfields. - Behavior: single-channel audio (voicemails) is transcribed directly; multi-channel audio (call recordings) is split per channel with ffmpeg, each channel is transcribed with word timestamps, and the channels are merged and interleaved by start time.
- Settings:
api_key,api_url,api_model,language,timestamps,sample_rate.
- URL: set
api_urlto the Azure region (for examplewesteurope). The code builds the Cognitive Services token endpoint and the conversation-recognition endpoint from it. - Auth:
api_keyis the Azure Cognitive Services subscription key. A token is requested first (Ocp-Apim-Subscription-Key), then used as aBearertoken for a long-running, detailed conversation transcription. - Settings:
api_key,api_url,language.
- URL:
https://speech.googleapis.com/v1p1beta1/speech(v1) orhttps://speech.googleapis.com/v2(v2). - Auth: the v1 endpoint appends
?key=<api_key>; the v2 endpoint authenticates withgcloud auth application-default print-access-token(sogcloudmust be installed and authenticated on the server). - Settings:
api_key,api_url.
- URL: the IBM Watson Speech-to-Text v3 endpoint (for example
https://api.<region>.speech-to-text.watson.cloud.ibm.com/v3/recognize). - Auth:
Authorization: Basic apikey:<api_key>. - Appends
×tamps=trueand supports model selection viaapi_model. - Settings:
api_key,api_url,api_model.
- No transcription / ffmpeg missing:
which ffmpegmust resolve. Install ffmpeg (see Requirements). Channel detection, segmentation, and Inworld conversion all depend on it. - Wrong API key or provider: check
transcribe.engineandtranscribe.api_key. Duplicate enabled rows for the same subcategory can make the loader pick the wrong value. After a database-only change, PHP-FPM with APCu may serve cached settings for a while. save_responseis your best debugging tool: set it totrue, run a job, and inspect thetranscribe_messagecolumn inv_transcribe_queuefor the raw provider response.- Multi-channel + diarization with OpenAI: the model is automatically switched to
whisper-1(the only OpenAI model that supportsdiarized_json). - Watson class name: both
transcribe_watson.phpandtranscribe_watson2.phpdeclareclass transcribe_watson. Only one is loadable at a time via the autoloader, so the effectivewatsonengine depends on which file the autoloader resolves. - Long recordings: the queue service resets
processingjobs older thanstale_timeout(default 1 hour) back topending, so a crashed worker does not wedge the queue.