Set up Azure live call transcription on a FreeSWITCH server

This guide covers cloning the project from GitLab (develop), building and installing the FreeSWITCH module, configuring the relay, starting services, and verifying the setup.

Working layout after setup

Item

Path

Relay app

/root/transcription_media_bug

Module

/var/lib/freeswitch/tmb/mod_azure_transcribe.so

Speech libs

/var/lib/freeswitch/tmb/lib/

Config

/root/transcription_media_bug/.env.azure

Service

tmb-relay-azure.service

How the pieces fit

Part

Where it runs

FreeSWITCH module (mod_azure_transcribe)

Inside FreeSWITCH. It attaches to call audio.

Microsoft Speech SDK

Also inside FreeSWITCH (libs under /var/lib/freeswitch/tmb/lib/). It sends audio to Azure Speech in the cloud.

Relay (tmb_app_azure.py)

Separate systemd service. Starts and stops transcription on bridge or hangup, and posts text to CCM. Light load.

Transcription starts when the call is bridged, not on early media.

Prerequisites

  • SSH access (root or sudo) on the FreeSWITCH server

  • FreeSWITCH running (fs_cli works)

  • GitLab access: https://gitlab.expertflow.com/rtc/media-bug-for-transcription

  • Packages: git, python3, python3-venv, python3-pip, gcc, g++, pkg-config, curl, tar you can use the below command

  • apt-get update && apt-get install -y git python3 python3-venv python3-pip gcc g++ pkg-config curl tar
    
  • Now please verify as well

  • dpkg-query -W -f='${Package}: ${Status}\n' git python3 python3-venv python3-pip gcc g++ pkg-config curl tar 2>&1
    
  • FreeSWITCH headers available on the server

pkg-config --cflags freeswitch
pkg-config --libs freeswitch

You will need for config: Azure Speech key, region (or endpoint), language, ESL password, CCM settings, and tenant domain names.

Step 1 — Clone the repo (develop)

The module source (.c / .cpp) is already in the repo under vendor/drachtio-freeswitch-modules/modules/mod_azure_transcribe/. A normal git clone of develop brings it. There is no separate module download.

The compiled .so and Microsoft Speech SDK binaries are not in Git. You still build those on the FreeSWITCH host (next steps), or copy a matching .so and lib/ from another host with the same FreeSWITCH architecture.

cd /root
git clone https://gitlab.expertflow.com/rtc/media-bug-for-transcription.git transcription_media_bug
cd /root/transcription_media_bug
git checkout develop
git pull origin develop

Confirm source is present:

pwd
ls tmb_app_azure.py scripts systemd relay .env.azure.example
ls vendor/drachtio-freeswitch-modules/modules/mod_azure_transcribe

You should see files such as mod_azure_transcribe.c, azure_transcribe_glue.cpp, and header files. If that folder is missing, you are on the wrong branch or an incomplete clone. Stop and fix before building.

Step 2 — Build and install the FreeSWITCH module

Build on an x86_64 Linux FreeSWITCH host (Speech SDK path lib/x64 below). FreeSWITCH development headers must be installed (pkg-config --cflags freeswitch).

2.1 Download the Microsoft Speech SDK

Bash
cd /root/transcription_media_bug
mkdir -p deps
curl -fsSL -L -o /tmp/SpeechSDK-Linux.tar.gz "https://aka.ms/csspeech/linuxbinary"
rm -rf deps/speechsdk
mkdir -p deps/speechsdk
tar -xzf /tmp/SpeechSDK-Linux.tar.gz -C deps/speechsdk --strip-components=1
ls deps/speechsdk/lib/x64 | head

If lib/x64 is missing, list deps/speechsdk/lib/ and use the architecture folder that exists on this host.

2.2 Compile

Bash
cd /root/transcription_media_bug
ROOT=/root/transcription_media_bug
SAFE=/var/lib/freeswitch/tmb
SDK_ROOT=$ROOT/deps/speechsdk
SRC=$ROOT/vendor/drachtio-freeswitch-modules/modules/mod_azure_transcribe
APP=$ROOT/build/azure/src
OUT=$ROOT/build/azure/out
OBJ=$ROOT/build/azure/obj

mkdir -p "$OUT" "$OBJ" "$SAFE/lib"
rm -rf "$APP"
mkdir -p "$APP"
cp -a "$SRC"/. "$APP"/

INC_CXX=$(find "$SDK_ROOT" -type d -name cxx_api | head -1)
INC_C=$(find "$SDK_ROOT" -type d -name c_api | head -1)
LIBDIR=$SDK_ROOT/lib/x64
FS_CFLAGS=$(pkg-config --cflags freeswitch)
FS_LIBS=$(pkg-config --libs freeswitch)

test -n "$INC_CXX" && test -n "$INC_C" && test -d "$LIBDIR"
test -f "$APP/mod_azure_transcribe.c"
test -f "$APP/azure_transcribe_glue.cpp"

gcc -fPIC -g -O2 $FS_CFLAGS -I"$APP" \
  -c "$APP/mod_azure_transcribe.c" -o "$OBJ/mod_azure_transcribe.o"
g++ -fPIC -g -O2 -std=c++14 $FS_CFLAGS -I"$APP" -I"$INC_CXX" -I"$INC_C" \
  -c "$APP/azure_transcribe_glue.cpp" -o "$OBJ/azure_transcribe_glue.o"
g++ -shared -o "$OUT/mod_azure_transcribe.so" \
  "$OBJ/mod_azure_transcribe.o" "$OBJ/azure_transcribe_glue.o" \
  -Wl,-rpath,"$SAFE/lib" -L"$LIBDIR" -lMicrosoft.CognitiveServices.Speech.core \
  $FS_LIBS -lpthread -lcrypto -lssl -lz

2.3 Install module and libs

Bash
cp -a "$LIBDIR"/. "$SAFE/lib/"
cp -a "$OUT/mod_azure_transcribe.so" "$SAFE/mod_azure_transcribe.so"

# FreeSWITCH user is often www-data — confirm with: ps aux | grep freeswitch
chown -R www-data:www-data "$SAFE"
chmod 755 "$SAFE/mod_azure_transcribe.so"

Check:

ls /var/lib/freeswitch/tmb/mod_azure_transcribe.so
ls /var/lib/freeswitch/tmb/lib | head
ldd /var/lib/freeswitch/tmb/mod_azure_transcribe.so

You should see mod_azure_transcribe.so, Speech libraries such as libMicrosoft.CognitiveServices.Speech.core.so, and ldd should not show those libs as missing.

Keep Speech libs only under /var/lib/freeswitch/tmb/lib/ (not under /usr/lib).

2.4 Load the module now

Use this server’s ESL password (replace YOUR_ESL_PASSWORD):

ESL_PW='YOUR_ESL_PASSWORD'
fs_cli -p "$ESL_PW" -x "unload mod_azure_transcribe" >/dev/null 2>&1 || true
fs_cli -p "$ESL_PW" -x "load /var/lib/freeswitch/tmb/mod_azure_transcribe.so"
fs_cli -p "$ESL_PW" -x "module_exists mod_azure_transcribe"

Must print true.

2.5 Autoload after FreeSWITCH restarts

Do this after .env.azure has the correct ESL_PASSWORD (Step 4), or export the password first:

cd /root/transcription_media_bug
# if .env.azure is not ready yet:
# export ESL_PASSWORD='YOUR_ESL_PASSWORD'
bash scripts/install_azure_autoload.sh

This installs a FreeSWITCH systemd drop-in so the module loads on every FreeSWITCH start (custom path plus library path for Speech libs).

Step 3 — Python venv

cd /root/transcription_media_bug
apt-get update
apt-get install -y python3-venv python3-pip
python3 -m venv venv
./venv/bin/pip install -U pip
./venv/bin/pip install -r relay/requirements.txt
./venv/bin/pip install aiohttp
./venv/bin/python -m py_compile tmb_app_azure.py

Step 4 — Configure .env.azure

cd /root/transcription_media_bug
cp -n .env.azure.example .env.azure
chmod 600 .env.azure
nano .env.azure

Key

What to set

TMB_ROOT

/root/transcription_media_bug

TMB_ENV_FILE

.env.azure

TMB_DISABLE_FILE

DISABLE.azure

TMB_MODE

observe first, then active

TMB_DOMAIN_ALLOWLIST

FreeSWITCH domain_name values (tenants), comma-separated. Example: thirdparty,paypal. Use * for all tenants on this switch

TMB_ALLOWLIST

Optional inbound DID filter inside those tenants. Prefer * unless you need to limit DIDs

TMB_ENABLE_INBOUND

true or false

TMB_ENABLE_MANUAL_OUTBOUND

true or false

TMB_ENABLE_CAMPAIGN

true or false

TMB_MAX_CONCURRENT

Start small (for example 3); raise after you size the host. With dual_mono, each call uses 2 Azure streams

TMB_TX_LAYOUT

dual_mono

TMB_LOG_BRIDGE_HEADERS

0 in production

ESL_HOST / ESL_PORT

127.0.0.1 / 8021

ESL_PASSWORD

ESL password for this FreeSWITCH

AZURE_SUBSCRIPTION_KEY

Azure Speech key

AZURE_REGION

Example: switzerlandnorth

AZURE_SERVICE_ENDPOINT

Matching endpoint if required

AZURE_LANG

Example: en-US

CCM_ROOT_DOMAIN

Example: intelicx.ai → CCM becomes https://{domain}.intelicx.ai/ccm

CCM_BASE_URL

Fallback when domain is missing

CCM_CONV_LOOKUP

true (default)

CCM_OUTBOUND_SERVICE_IDENTIFIER

Leave blank. Outbound service id comes from the call header X-Destination-Number. Do not set this to an inbound Branch DN

How control works

  1. Tenants → TMB_DOMAIN_ALLOWLIST

  2. Call types → TMB_ENABLE_*

  3. Optional inbound DIDs → TMB_ALLOWLIST

  4. CCM host → FS domain_name + CCM_ROOT_DOMAIN

Example:

TMB_MODE=active
TMB_DOMAIN_ALLOWLIST=thirdparty
TMB_ALLOWLIST=*
TMB_ENABLE_INBOUND=true
TMB_ENABLE_MANUAL_OUTBOUND=true
TMB_ENABLE_CAMPAIGN=true
CCM_ROOT_DOMAIN=intelicx.ai
CCM_BASE_URL=https://thirdparty.intelicx.ai/ccm

Check without printing secrets:

Bash
grep -E '^(TMB_MODE|TMB_DOMAIN_ALLOWLIST|TMB_ALLOWLIST|TMB_ROOT|ESL_HOST|ESL_PORT|AZURE_REGION|AZURE_LANG|CCM_ROOT_DOMAIN|CCM_BASE_URL)=' .env.azure
grep -q '^AZURE_SUBSCRIPTION_KEY=.\+' .env.azure && echo KEY=PRESENT || echo KEY=MISSING
grep -q '^ESL_PASSWORD=.\+' .env.azure && echo ESL_PASSWORD=PRESENT || echo ESL_PASSWORD=MISSING

If you have not run Step 2.5 yet, install module autoload now:

cd /root/transcription_media_bug
bash scripts/install_azure_autoload.sh

Step 5 — Install and start the relay

cd /root/transcription_media_bug
cp systemd/tmb-relay-azure.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable tmb-relay-azure
systemctl restart tmb-relay-azure
systemctl status tmb-relay-azure --no-pager
journalctl -u tmb-relay-azure -n 40 --no-pager

Must be active (running).

Step 6 — Go live and test

  1. Set TMB_MODE=active in .env.azure

  2. Restart the relay

  3. Make a bridged call on an allowed tenant and speak

  4. Watch the logs

systemctl restart tmb-relay-azure
journalctl -u tmb-relay-azure -f

You should see BRIDGE, start tx, FINAL, and CCM ok. Text should show on the agent desk on the same conversation as the call (not a second inbound topic).

Outbound CCM fields

Field

Source

Conversation id

sip_h_X-CALL-ID

Service identifier

sip_h_X-Destination-Number

Customer identifier

Customer number on the call

Inbound

Field

Source

Conversation id

Channel / Voice Connector conversation id when present

Service identifier

Inbound service DN / headers

Customer identifier

Real caller ANI

Final health check

Bash
cd /root/transcription_media_bug
ls tmb_app_azure.py
ls /var/lib/freeswitch/tmb/mod_azure_transcribe.so
ls /var/lib/freeswitch/tmb/lib | head
systemctl is-active tmb-relay-azure
fs_cli -x "module_exists mod_azure_transcribe"

Expected: app present, module and libs present, relay active, module true.

Useful commands

systemctl status tmb-relay-azure
journalctl -u tmb-relay-azure -f
systemctl restart tmb-relay-azure

touch /root/transcription_media_bug/DISABLE.azure && systemctl restart tmb-relay-azure
rm -f /root/transcription_media_bug/DISABLE.azure && systemctl restart tmb-relay-azure

fs_cli -x "module_exists mod_azure_transcribe"
fs_cli -x "unload mod_azure_transcribe"
fs_cli -x "load /var/lib/freeswitch/tmb/mod_azure_transcribe.so"

After a relay-only git pull: restart tmb-relay-azure.
After a module rebuild: copy .so and libs, then unload and load the module.

Capacity

dual_mono uses two Azure streams per bridged call. Keep TMB_MAX_CONCURRENT in line with Azure quota and server CPU/RAM.

Things to avoid

  1. Do not put Speech libs under /usr/lib or /usr/local/lib

  2. Do not run make install on FreeSWITCH only for this module

  3. Do not run Deepgram and Azure relays on the same traffic together

  4. Do not commit Azure keys or ESL passwords to Git

  5. Do not set CCM_OUTBOUND_SERVICE_IDENTIFIER to an inbound Branch DN

Checklist

  • Cloned develop; module source present under vendor/.../mod_azure_transcribe/

  • Speech SDK downloaded; module built (.so is not in Git)

  • Module .so and lib/ under /var/lib/freeswitch/tmb/; ldd OK

  • module_exists = true; autoload installed with the correct ESL password

  • venv ready; .env.azure filled (domain allowlist, CCM root, Azure, ESL; outbound SI env blank)

  • tmb-relay-azure active

  • Bridged test call shows FINAL and desk text on one conversation